The "Error Handling System" workflow (category: Development) has been executed for the generate step using the collab app.
Language Specified: JavaScript
Error Types Focus: Test Error Types
This output provides a comprehensive guide to designing and implementing a robust error handling system in JavaScript, with a specific focus on incorporating and testing "Test Error Types" to ensure system resilience and correctness.
Effective error handling is crucial for building reliable and maintainable applications. It ensures that applications can gracefully recover from unexpected situations, provide meaningful feedback to users and developers, and prevent unhandled exceptions from crashing the application. In JavaScript, this involves a combination of synchronous and asynchronous error management, custom error types, and strategic testing.
This guide will cover the fundamental principles, common patterns, and best practices for creating a resilient error handling system in JavaScript, with a particular emphasis on how to define, implement, and rigorously test various error scenarios, including those categorized as "Test Error Types."
try...catch...finallyThe try...catch...finally statement is the cornerstone of synchronous error handling in JavaScript.
try block: Contains the code that might throw an exception.catch block: Executes if an exception is thrown in the try block. It receives the error object as an argument.finally block: Executes regardless of whether an exception was thrown or caught. It's ideal for cleanup operations.Actionable Detail: Always wrap potentially failing synchronous operations within try...catch blocks at appropriate boundaries (e.g., function entry points, I/O operations).
Asynchronous operations introduce complexities. Traditional try...catch doesn't directly work across asynchronous boundaries without specific patterns.
Promises have built-in error handling mechanisms:
.catch(): Catches errors from any preceding .then() or the initial Promise constructor..finally(): Executes after the Promise settles (either resolved or rejected), similar to finally.Actionable Detail: Always chain a .catch() to the end of your Promise chains to prevent unhandled promise rejections.
async/awaitasync/await syntax, while making asynchronous code look synchronous, still requires try...catch for error handling. An await call can throw an error, which can then be caught by a surrounding try...catch block.
Actionable Detail: Wrap await calls in try...catch blocks within your async functions to handle potential rejections from awaited Promises.
Native JavaScript errors (e.g., Error, TypeError, RangeError) are often insufficient to convey specific application-level issues. Custom error classes allow you to:
error instanceof MyCustomError).Actionable Detail: Define custom error classes for distinct application-specific failure modes (e.g., ValidationError, NetworkError, AuthenticationError, ResourceNotFoundError). Extend from Error to inherit standard error properties like name and stack.
For unhandled exceptions that propagate up to the top level, global error handlers provide a last resort mechanism to log errors and prevent application crashes (in Node.js) or provide a fallback UI (in browsers).
window.onerror and window.addEventListener('unhandledrejection', ...)process.on('uncaughtException', ...) and process.on('unhandledRejection', ...)Actionable Detail: Implement global error handlers to catch any errors that escape local try...catch blocks. These handlers should primarily log the error, potentially notify monitoring systems, and perform graceful shutdown/degradation if necessary, but should generally not attempt to recover from the error.
"Test Error Types" refers to defining specific error conditions that are critical to test and verify your error handling system. This section details how to create and leverage these types of errors for robust testing.
Consider error types that represent common failure points or specific business logic violations. These can be used both in production code and for simulating errors in tests.
Structured Data: Example Custom Error Types
### 3.3. Assertions for Error Handling
Your tests should not only simulate errors but also verify that the error handling logic correctly:
* **Catches the error**: The `catch` block is executed.
* **Transforms the error**: The raw error is converted into a more meaningful custom error (e.g., a low-level DB error into a `ResourceNotFoundError`).
* **Logs the error**: The error is passed to the logging mechanism with appropriate severity and context.
* **Provides correct user feedback**: In UI contexts, the user sees an appropriate message.
* **Returns correct HTTP status codes**: In API contexts, the server responds with the expected status code.
**Actionable Detail:** Use `expect(...).rejects.toThrow(...)` or `expect(() => {...}).toThrow(...)` for asserting thrown errors. Verify the error type using `toBeInstanceOf()` and check specific properties of the error object.
---
## 4. Best Practices and Recommendations
### 4.1. Centralized Error Processing
* **Recommendation:** Create a central error handling middleware (for web frameworks like Express) or a utility function (`errorHandler.js`) that processes all caught errors. This function can log, format, and potentially send alerts.
* **Actionable Detail:** Your centralized handler should differentiate between operational errors (predictable, handled, e.g., `ValidationError`) and programmer errors (unpredictable, unhandled, e.g., `ReferenceError`).
### 4.2. Consistent Logging
* **Recommendation:** Use a dedicated logging library (e.g., Winston, Pino) with different log levels (debug, info, warn, error, fatal).
* **Actionable Detail:** Always log errors with sufficient context (user ID, request ID, relevant input parameters, stack trace) to aid in debugging. Mask sensitive information.
### 4.3. User Feedback and Graceful Degradation
* **Recommendation:** Distinguish between errors that can be shown to the user and internal errors.
* **Actionable Detail:** For user-facing errors, provide clear, concise, and helpful messages. For internal errors, provide a generic message and log the details for developers. Implement fallback UIs or features when certain services are unavailable.
### 4.4. Idempotency and Retries
* **Recommendation:** Design operations to be idempotent where possible, meaning performing them multiple times has the same effect as performing them once.
* **Actionable Detail:** For transient errors (e.g., network issues, temporary service unavailability), implement retry mechanisms with exponential backoff.
### 4.5. Security Considerations
* **Recommendation:** Never expose sensitive internal error details (stack traces, database connection strings, internal server paths) directly to clients.
* **Actionable Detail:** Sanitize error messages before sending them to the client. Map internal errors to generic, safe error messages for external consumption.
### 4.6. Monitoring and Alerting
* **Recommendation:** Integrate with application performance monitoring (APM) tools (e.g., Sentry, New Relic, Datadog) to track error rates and types.
* **Actionable Detail:** Set up alerts for critical error thresholds or specific error types to proactively identify and address issues.
### 4.7. Error Boundary Components (React/UI Frameworks)
* **Recommendation:** For UI applications, use error boundaries (if your framework supports them) to catch errors in child components and display a fallback UI, preventing the entire application from crashing.
---
## 5. Structured Data: Example Error Handling Flow
This example illustrates a common error handling flow in a Node.js Express application, incorporating custom errors and centralized handling.
This comprehensive approach will significantly improve the stability, maintainability, and diagnostic capabilities of your JavaScript application.
Workflow Name: Error Handling System
Category: Development
Language: JavaScript
Error Types Handled (Example): Test Error Types
This document outlines a comprehensive and professional error handling system for JavaScript applications, designed for robustness, maintainability, and enhanced debugging capabilities. It incorporates best practices for both frontend and backend JavaScript environments, emphasizing custom error types, centralized handling, logging, and reporting.
A robust error handling system is crucial for the stability, reliability, and user experience of any application. This system aims to:
This documentation serves as a guide for understanding, implementing, and utilizing the error handling capabilities within your JavaScript projects.
The error handling system is built upon several core concepts:
Error object to create domain-specific error types. This allows for programmatic identification and handling of different error conditions.The error handling system follows a layered architecture to ensure separation of concerns and modularity:
process.on('uncaughtException') in Node.js, window.onerror in browsers).console, Winston, Pino, CloudWatch).
graph TD
A[Application Code] --> B{throw new CustomError()};
B --> C[try...catch Block];
C --> D[Error Utility Service];
A --> G[Unhandled Error];
G --> H[Global Error Handlers];
H --> D;
D --> E[Logging Service];
D --> F[Reporting Service];
D --> I[API/UI Response Layer];
Custom error classes extend the native Error object, allowing for semantic error identification and structured data attachment.
Base Error Class (src/errors/BaseError.js)
class BaseError extends Error {
constructor(message, context = {}, statusCode = 500, isOperational = true) {
super(message);
this.name = this.constructor.name;
this.context = context; // Additional debugging context
this.statusCode = statusCode; // HTTP status code for API responses
this.isOperational = isOperational; // Flag for operational vs. programming errors
Error.captureStackTrace(this, this.constructor); // Captures stack trace
}
// Static method to check if an error is an instance of BaseError or its derivatives
static isInstance(error) {
return error instanceof BaseError;
}
}
export { BaseError };
Specific Error Types (e.g., for "Test Error Types") (src/errors/TestErrors.js)
We'll define specific error types related to "Test Error Types" to illustrate how to categorize errors programmatically.
import { BaseError } from './BaseError';
/**
* Base class for all test-related errors.
*/
class TestError extends BaseError {
constructor(message, context = {}, statusCode = 500, isOperational = true) {
super(message, context, statusCode, isOperational);
this.errorCategory = 'TEST_ERROR'; // Custom category flag
}
}
/**
* Represents a validation failure during a test operation.
*/
class TestValidationError extends TestError {
constructor(message = 'Test validation failed.', validationDetails = [], context = {}) {
super(message, { ...context, validationDetails }, 400, true); // 400 Bad Request
this.name = 'TestValidationError';
this.validationDetails = validationDetails; // Specific details about validation failures
}
}
/**
* Represents an error related to test configuration.
*/
class TestConfigurationError extends TestError {
constructor(message = 'Invalid test configuration.', configKey, context = {}) {
super(message, { ...context, configKey }, 500, false); // 500 Internal Server Error, typically non-operational for user
this.name = 'TestConfigurationError';
this.configKey = configKey; // The problematic configuration key
}
}
/**
* Represents an error during the execution of a test (e.g., test runner issue).
*/
class TestExecutionError extends TestError {
constructor(message = 'Test execution failed unexpectedly.', testId, context = {}) {
super(message, { ...context, testId }, 500, false);
this.name = 'TestExecutionError';
this.testId = testId;
}
}
export { TestError, TestValidationError, TestConfigurationError, TestExecutionError };
This service acts as the primary interface for processing errors.
(src/utils/errorHandler.js)
import { BaseError } from '../errors/BaseError';
import { logger } from './logger'; // Assuming a logger utility
import { errorReporter } from './errorReporter'; // Assuming an error reporting utility
class ErrorHandler {
/**
* Handles an error by logging it and optionally reporting it.
* This is the primary method to call when catching an error.
* @param {Error} error The error object.
* @param {Object} [additionalContext={}] Additional context to log/report.
*/
handleError(error, additionalContext = {}) {
logger.error(`Error handled: ${error.message}`, {
name: error.name,
stack: error.stack,
context: error.context,
additionalContext,
isOperational: error.isOperational,
statusCode: error.statusCode,
});
// Report non-operational errors and critical operational errors to external services
if (!error.isOperational || error.statusCode >= 500) {
errorReporter.report(error, additionalContext);
}
// In a server environment, you might want to restart the process for critical non-operational errors
// In a client environment, you might display a generic error message
if (!error.isOperational) {
console.error('CRITICAL NON-OPERATIONAL ERROR. Application might be in an unstable state.');
// For server: process.exit(1); // Consider graceful shutdown before exiting
// For client: display a fatal error screen and prompt refresh
}
}
/**
* Determines if an error is operational (expected and handled gracefully) or programming (unexpected).
* @param {Error} error The error object.
* @returns {boolean} True if the error is operational, false otherwise.
*/
isOperationalError(error) {
return error instanceof BaseError && error.isOperational;
}
/**
* Formats an error for API responses.
* @param {Error} error The error object.
* @returns {{statusCode: number, message: string, code?: string, details?: any}} Formatted error object.
*/
formatErrorForResponse(error) {
if (this.isOperationalError(error)) {
// Operational errors: provide user-friendly message and relevant details
return {
statusCode: error.statusCode,
message: error.message,
code: error.name,
details: error.context,
};
} else {
// Programming errors: hide internal details, provide generic message
return {
statusCode: 500,
message: 'An unexpected error occurred.',
code: 'INTERNAL_SERVER_ERROR',
details: process.env.NODE_ENV === 'development' ? { stack: error.stack, name: error.name } : undefined,
};
}
}
}
const errorHandler = new ErrorHandler();
export { errorHandler };
These mechanisms prevent the application from crashing due to uncaught exceptions or unhandled promise rejections.
Node.js (src/app.js or src/server.js)
import { errorHandler } from './utils/errorHandler';
import { logger } from './utils/logger'; // Make sure logger is initialized
// Catch unhandled synchronous exceptions
process.on('uncaughtException', (error) => {
logger.fatal('Uncaught Exception! Shutting down...', { errorName: error.name, errorMessage: error.message, stack: error.stack });
errorHandler.handleError(error, { type: 'uncaughtException' });
// For critical non-operational errors, often best to exit and let a process manager restart
if (!errorHandler.isOperationalError(error)) {
process.exit(1);
}
});
// Catch unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
logger.fatal('Unhandled Rejection! Shutting down...', { reason, promise });
// Rejections can be anything, wrap in an Error if it's not already one
const error = (reason instanceof Error) ? reason : new Error(`Unhandled Rejection: ${reason}`);
errorHandler.handleError(error, { type: 'unhandledRejection', promise });
// For critical non-operational errors, often best to exit and let a process manager restart
if (!errorHandler.isOperationalError(error)) {
process.exit(1);
}
});
// Example of integrating with an Express app (server-side)
// This middleware should be the LAST middleware added to your Express app
const errorMiddleware = (err, req, res, next) => {
errorHandler.handleError(err, { requestUrl: req.originalUrl, method: req.method, body: req.body, user: req.user?.id });
const formattedError = errorHandler.formatErrorForResponse(err);
res.status(formattedError.statusCode).json({
status: 'error',
message: formattedError.message,
code: formattedError.code,
details: formattedError.details,
});
};
export { errorMiddleware };
Browser (Client-side src/index.js or main entry file)
import { errorHandler } from './utils/errorHandler';
// Catch unhandled errors (syntax errors, runtime errors)
window.onerror = function (message, source, lineno, colno, error) {
console.error('Window.onerror caught:', { message, source, lineno, colno, error });
errorHandler.handleError(error || new Error(message), {
type: 'window.onerror',
source,
lineno,
colno
});
return true; // Prevent default browser error handling
};
// Catch unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled Rejection caught:', event.reason);
const error = (event.reason instanceof Error) ? event.reason : new Error(`Unhandled Rejection: ${event.reason}`);
errorHandler.handleError(error, { type: 'unhandledRejection' });
event.preventDefault(); // Prevent default browser handling
});
A dedicated logging utility for structured and contextual logging.
(src/utils/logger.js)
// Example using console, but can be replaced with Winston, Pino, etc.
class Logger {
log(level, message, metadata = {}) {
const timestamp = new Date().toISOString();
const logEntry = { timestamp, level, message, ...metadata };
switch (level) {
case 'debug':
console.debug(JSON.stringify(logEntry));
break;
case 'info':
console.info(JSON.stringify(logEntry));
break;
case 'warn':
console.warn(JSON.stringify(logEntry));
break;
case 'error':
console.error(JSON.stringify(logEntry));
break;
case 'fatal':
console.error(JSON.stringify(logEntry));
break;
default:
console.log(JSON.stringify(logEntry));
}
}
debug(message, metadata) { this.log('debug', message, metadata); }
info(message, metadata) { this.log('info', message, metadata); }
warn(message, metadata) { this.log('warn', message, metadata); }
error(message, metadata) { this.log('error', message, metadata); }
fatal(message, metadata) { this.log('fatal', message, metadata); }
}
const logger = new Logger();
export { logger };
// In a real application, you might use a library like Winston or Pino
/*
import winston from 'winston';
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.json(),
transports: [
new winston.transports.Console(),
// new winston.transports.File({ filename: 'error.log', level: 'error' }),
// new winston.transports.File({ filename: 'combined.log' }),
],
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple(),
}));
}
export { logger };
*/
Connects with external error monitoring services.
(src/utils/errorReporter.js)
// Example using a placeholder, can be Sentry, Bugsnag, etc.
class ErrorReporter {
constructor() {
// Initialize your reporting service here (e.g., Sentry.init)
if (process.env.SENTRY_DSN) {
// import * as Sentry from '@sentry/node'; // or '@sentry/browser'
// Sentry.init({ dsn: process.env.SENTRY_DSN });
console.log('Sentry initialized (placeholder)');
this.isInitialized = true;
} else {
console.warn('SENTRY_DSN not set. Error reporting disabled.');
this.isInitialized = false;
}
}
/**
* Reports an error to an external service.
* @param {Error} error The error object.
* @param {Object} [context={}] Additional context to send with the report.
*/
report(error, context = {}) {
if (!this.isInitialized) {
console.warn('Error reporter not initialized. Error not reported externally.');
return;
}
console.log(`Reporting error to external service: ${error.name} - ${error.message}`, {
error,
context: {
...error.context,
...context,
isOperational: error.isOperational,
statusCode: error.statusCode,
},
stack: error.stack,
});
// Example for Sentry:
// Sentry.withScope((scope) => {
// scope.setExtras({ ...error.context, ...context });
// if (error.statusCode) {
// scope.setTag('statusCode', error.statusCode);
// }
// if (error.errorCategory) {
// scope.setTag('category', error.errorCategory);
// }
// Sentry.captureException(error);
// });
}
/**
* Sets user context for error reporting (e.g., for Sentry).
* @param {Object} user User information (id, email, username).
*/
setUserContext(user) {
if (this.isInitialized) {
// Sentry.setUser({ id: user.id, email: user.email, username: user.username });
console.log('User context set for reporting:', user);
}
}
/**
* Clears user context for error reporting.
*/
clearUserContext() {
if (this.isInitialized) {
// Sentry.setUser(null);
console.log('User context cleared for reporting.');
}
}
}
const errorReporter = new ErrorReporter();
export { errorReporter };
Always throw custom errors for business logic failures.
import { TestValidationError, TestConfigurationError, TestExecutionError } from './errors/TestErrors';
function validateTestParameters(params) {
if (!params || !params.testId) {
throw new TestValidationError('Missing required test ID.', [{ field: 'testId', message: 'Test ID is required.' }], { userId: 'user123' });
}
if (params.testId.length < 5) {
throw new TestValidationError('Test ID is too short.', [{ field: 'testId', message: 'Minimum length is 5.' }], { userId: 'user123' });
}
return true;
}
function loadTestConfig(configKey) {
if (!process.env[configKey]) {
throw new TestConfigurationError(`Environment variable for ${configKey} is not set.`, configKey, { service: 'TestRunner' });
}
return process.env[configKey];
}
async function runTest(testId) {
// Simulate an external API call or complex logic that might fail
try {
const response = await fetch(`https://api.testservice.com/run/${testId}`);
if (!response.ok) {
const errorData = await response.json();
throw new TestExecutionError(`Failed to execute test ${testId} via external service.`, testId, { apiResponse: errorData, httpStatus: response.status });
}
return await response.json();
} catch (e) {
// Catch network errors or parsing errors from fetch
throw new TestExecutionError(`Network or parsing error during test execution for ${testId}.`, testId, { originalError: e.message });
}
}
Use try...catch blocks to catch errors and pass them to the central errorHandler.
import { errorHandler } from './utils/errorHandler';
import { TestValidationError, TestConfigurationError, TestExecutionError } from './errors/TestErrors';
async function processTestRequest(req, res) {
const { testId, testParams } = req.body; // Example for an API request
try {
validateTestParameters(testParams); // Might throw TestValidationError
const config = loadTestConfig('TEST_API_KEY'); // Might throw TestConfigurationError
const testResult = await runTest(testId); // Might throw TestExecutionError
res.status(200).json({ status: 'success', data: testResult });
} catch (error) {
// Pass all caught errors to the central handler
errorHandler.handleError(error, { userId: req.user?.id, requestId: req.id });
// For API responses, format the error appropriately
const formattedError = errorHandler.formatErrorForResponse(error);
res.status(formattedError.statusCode).json({
status: 'error',
message: formattedError.message,
code: formattedError.code,
details: formattedError.details,
});
// Example of specific handling for a TestValidationError (e.g., for UI feedback)
if (error instanceof TestValidationError) {
console.log(`UI should show specific validation errors: ${JSON.stringify(error.validationDetails)}`);
}
}
}
// Example usage
// Assuming req and res objects from an Express-like framework
// processTestRequest({ body: { testId: 'abc', testParams: {} } }, {}); // Will throw TestValidationError
// processTestRequest({ body: { testId: 'valid-id', testParams: { testId: 'valid-id' } } }, {}); // Will proceed if config and runTest don't fail
This section demonstrates how the system categorizes and handles the custom Test Error Types defined earlier.
Error Differentiation and Response Mapping:
The errorHandler.formatErrorForResponse method, combined with the BaseError.isOperational flag and statusCode, provides a clear mechanism for differentiating errors and generating appropriate responses.
| Error Type | isOperational | statusCode | Typical API Response | Logging/Reporting |
| :---------------------- | :-------------- | :----------- | :--------------------------------------------------------------------------------- | :---------------------------------------------------- |
| TestValidationError | true | 400 | Specific validation messages, details with validationDetails. | Logged, not always reported externally (unless critical) |
| TestConfigurationError| false | 500 | Generic "Internal Server Error" (hides configKey in production), code: 'INTERNAL_SERVER_ERROR'. | Logged, always reported externally. |
| TestExecutionError | false | 500 | Generic "Internal Server Error" (hides originalError in production). | Logged, always reported externally. |
| Native Error | false | 500 | Generic "Internal Server Error". | Logged, always reported externally. |
Actionable Insight: By using custom error classes and the isOperational flag, you can distinguish between errors that are part of normal application flow (e.g., user input errors) and those that indicate a bug or system failure, allowing for different logging, reporting, and user feedback strategies.
The error handling system's behavior can be configured via environment variables or a dedicated configuration file.
// Example configuration in a 'config.js' or '.env' file
// .env
// NODE_ENV=development
// LOG_LEVEL=debug
// SENTRY_DSN=https://examplepublickey@o0.ingest.sentry.io/0
// config.js
export const config = {
env: process.env.NODE_ENV || 'development',
logLevel: process.env.LOG_LEVEL || 'info',
sentry: {
dsn: process.env.SENTRY_DSN || null,
enabled: !!process.env.SENTRY_DSN,
// Other Sentry options
},
// Add other relevant configurations like error display messages
errorMessages: {
genericInternalError: 'An unexpected error occurred. Please try again later.',
// ...
}
};
Recommendations:
development, staging, and production environments.isOperational flag to differentiate between expected, recoverable errors (e.g., validation) and unexpected bugs (e.g., null pointer).errorHandler to ensure consistency.finally blocks or error handlers, ensure resources (e.g., database connections, file handles) are properly closed or released.This section outlines the primary public methods of the errorHandler and errorReporter for quick reference.
errorHandlererrorHandler.handleError(error: Error, additionalContext?: object): The primary method to process any caught error. Logs the error, and conditionally reports it.errorHandler.isOperationalError(error: Error): boolean: Checks if an error is an instance of BaseError and marked as operational.errorHandler.formatErrorForResponse(error: Error): { statusCode: number, message: string, code?: string, details?: any }: Prepares an error object for sending as an API response, sanitizing sensitive details for non-operational errors.errorReportererrorReporter.report(error: Error, context?: object): Sends an error to the configured external error monitoring service (e.g., Sentry).errorReporter.setUserContext(user: { id: string, email?: string, username?: string }): Associates user information with subsequent error reports.errorReporter.clearUserContext(): Clears any previously set user context.\n