advanced-error-handling-nodejs
Advanced Error Handling in TypeScript Node.js:
For each of these advanced TypeScript concepts, I'll provide an explanation and example:
Custom Error Classes:
Extend the
Error
class to create specific error types.Example:
class DatabaseError extends Error {
constructor(message: string, public code: number) {
super(message);
this.name = 'DatabaseError';
}
}
class ValidationError extends Error {
constructor(message: string, public fields: string[]) {
super(message);
this.name = 'ValidationError';
}
}
Typed Error Handling:
Use type guards within catch blocks to handle errors based on their type.
- Example:
try {
// some operation that might throw
} catch (error) {
if (error instanceof DatabaseError) {
// handle database error
} else if (error instanceof ValidationError) {
// handle validation error
} else {
// handle generic or unknown error
}
}
- Example:
Using
instanceof
for Error Checking:Determine the type of an error using
instanceof
.- Example:
try {
// operation that might throw
} catch (error) {
if (error instanceof DatabaseError) {
console.error('Database error code:', error.code);
}
}
- Example:
Union Types for Errors:
Annotate functions with union types to represent all possible thrown errors.
Example:
function riskyOperation(): void | never {
throw new DatabaseError('Failed to connect', 500);
}
function handleOperation() {
try {
riskyOperation();
} catch (error) {
if (error instanceof DatabaseError || error instanceof ValidationError) {
// handle known errors
}
}
}
Error Boundary Middleware:
Implement error-handling middleware in Express to catch and process errors from all routes.
- Example:
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
if (err instanceof DatabaseError) {
res.status(500).send('Database error occurred');
} else if (err instanceof ValidationError) {
res.status(400).send('Validation failed');
} else {
res.status(500).send('An unexpected error occurred');
}
});
- Example:
Async Error Handling:
Use a higher-order function to wrap async functions and handle errors.
Example:
const catchAsyncErrors = (fn: Function) =>
(req: Request, res: Response, next: NextFunction) =>
fn(req, res, next).catch(next);
// Usage with an async route handler
app.get('/data', catchAsyncErrors(async (req, res) => {
// Async operations that might throw
}));
Each of these strategies leverages TypeScript's type system to improve error handling, making your code more robust and easier to maintain.
Let's explore these concepts with explanations and examples:
Never Throwing Raw Errors:
Use specific error types for throw statements to enhance error context.
- Example:
// Instead of throw new Error("Invalid input");
throw new ValidationError('Invalid input', ['username', 'password']);
- Example:
Error Logging:
Implement logging that captures error details, including stack traces.
- Example:
function logError(error: Error) {
console.error(error.message);
console.error('Stack Trace:', error.stack);
}
- Example:
Global Exception Handling:
Capture uncaught exceptions at the process level to manage unexpected errors.
- Example:
process.on('uncaughtException', (error) => {
logError(error);
process.exit(1); // Exit after logging
});
- Example:
Handling Promise Rejections:
Listen for unhandled promise rejections and handle them globally.
- Example:
process.on('unhandledRejection', (reason, promise) => {
logError(reason instanceof Error ? reason : new Error(reason));
});
- Example:
Type Guards for Error Properties:
Check for custom properties on errors using type guards.
- Example:
catch (error) {
if ('code' in error) {
const errorCode = (error as { code: number }).code;
// Handle based on errorCode
}
}
- Example:
Rich Error Information:
Add context to errors using additional properties.
- Example:
class NetworkError extends Error {
constructor(message: string, public status: number, public data: any) {
super(message);
this.name = 'NetworkError';
}
}
- Example:
Error Transformation for Clients:
Format errors before sending them to client-side, removing sensitive data.
- Example:
function transformErrorForClient(error: Error) {
return {
message: error.message,
// Exclude sensitive properties like 'stack'
};
}
- Example:
Error Propagation:
Decide whether to handle errors or let them bubble up to the caller.
- Example:
function mightFail() {
try {
// risky operation
} catch (error) {
if (shouldHandleLocally(error)) {
handleLocally(error);
} else {
throw error; // propagate
}
}
}
- Example:
Testing Error Handling:
Test error handling paths using mock errors and TypeScript's type assertions.
- Example:
describe('Error handling', () => {
it('handles custom errors correctly', () => {
const testError = new ValidationError('Test error', ['test']);
expect(() => {
throw testError;
}).toThrow(ValidationError);
});
});
- Example:
These practices help in creating a robust error handling system that utilizes TypeScript's static typing to manage and track errors effectively throughout the application.