|
Error Handling in Node.js is crucial for building robust applications. This guide covers error types, patterns, and best practices.
|
|
|
Error Types in Node.js
Node.js has several built-in error types.
Standard JavaScript errors
|
const syntaxError = new SyntaxError('Invalid syntax');
const typeError = new TypeError('Expected a string');
const rangeError = new RangeError('Value out of range');
const referenceError = new ReferenceError('Variable not defined');
console.log('Error types:', {
syntaxError: syntaxError.name,
typeError: typeError.name,
rangeError: rangeError.name
});
|
|
System Errors
Node.js system errors include additional properties.
|
import { readFileSync } from 'node:fs';
try {
readFileSync('/nonexistent/file.txt');
} catch (err) {
console.log('System Error Properties:');
console.log(' name:', err.name);
console.log(' message:', err.message);
console.log(' code:', err.code);
console.log(' syscall:', err.syscall);
console.log(' path:', err.path);
console.log(' errno:', err.errno);
}
|
|
Creating Custom Errors
Extend the Error class for application-specific errors.
Using custom errors
|
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = 'ValidationError';
this.field = field;
this.statusCode = 400;
Error.captureStackTrace(this, ValidationError);
}
}
class NotFoundError extends Error {
constructor(resource, id) {
super(`${resource} with id '${id}' not found`);
this.name = 'NotFoundError';
this.resource = resource;
this.id = id;
this.statusCode = 404;
Error.captureStackTrace(this, NotFoundError);
}
}
class AuthenticationError extends Error {
constructor(message = 'Authentication required') {
super(message);
this.name = 'AuthenticationError';
this.statusCode = 401;
Error.captureStackTrace(this, AuthenticationError);
}
}
function validateUser(user) {
if (!user.email) {
throw new ValidationError('email', 'Email is required');
}
if (!user.email.includes('@')) {
throw new ValidationError('email', 'Invalid email format');
}
}
try {
validateUser({ name: 'Alice' });
} catch (err) {
if (err instanceof ValidationError) {
console.log(`Validation failed on '${err.field}': ${err.message}`);
}
}
|
|
Try-Catch for Synchronous Code
Handle errors in synchronous operations.
|
function parseJSON(jsonString) {
try {
const data = JSON.parse(jsonString);
return { success: true, data };
} catch (err) {
return { success: false, error: err.message };
}
}
console.log(parseJSON('{"valid": true}'));
console.log(parseJSON('invalid json'));
|
|
Async/Await Error Handling
Use try-catch with async functions.
|
import { readFile } from 'node:fs/promises';
async function loadConfig(path) {
try {
const content = await readFile(path, 'utf8');
return JSON.parse(content);
} catch (err) {
if (err.code === 'ENOENT') {
console.log('Config file not found, using defaults');
return { default: true };
}
throw err;
}
}
|
|
Promise Error Handling
Handle errors in Promise chains.
Using .catch()
Using async/await
|
function fetchUser(id) {
return new Promise((resolve, reject) => {
if (id <= 0) {
reject(new ValidationError('id', 'ID must be positive'));
} else if (id > 1000) {
reject(new NotFoundError('User', id));
} else {
resolve({ id, name: 'User ' + id });
}
});
}
fetchUser(-1)
.then(user => console.log(user))
.catch(err => console.log('Error:', err.message));
async function getUser(id) {
try {
const user = await fetchUser(id);
return user;
} catch (err) {
console.log('Failed to get user:', err.message);
return null;
}
}
|
|
Error-First Callbacks
Traditional Node.js callback pattern.
|
import { stat } from 'node:fs';
stat('./package.json', (err, stats) => {
if (err) {
console.error('Stat error:', err.message);
return;
}
console.log('File size:', stats.size);
});
|
|
EventEmitter Error Handling
Handle errors emitted by EventEmitters.
Always add an error handler to prevent crashes
|
import { EventEmitter } from 'node:events';
const emitter = new EventEmitter();
emitter.on('error', (err) => {
console.error('Emitter error:', err.message);
});
emitter.emit('error', new Error('Something went wrong'));
|
|
Process-Level Error Handling
Catch unhandled errors at the process level.
Unhandled promise rejections
Uncaught exceptions
|
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection:', reason);
});
process.on('uncaughtException', (err, origin) => {
console.error('Uncaught Exception:', err);
console.error('Origin:', origin);
process.exit(1);
});
|
|
Graceful Error Recovery
Implement retry logic for transient errors.
Usage:
await withRetry(() => fetchData(url), { maxRetries: 5, delay: 500 });
|
async function withRetry(fn, options = {}) {
const { maxRetries = 3, delay = 1000, backoff = 2 } = options;
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (err) {
lastError = err;
console.log(`Attempt ${attempt} failed: ${err.message}`);
if (attempt < maxRetries) {
const waitTime = delay * Math.pow(backoff, attempt - 1);
console.log(`Retrying in ${waitTime}ms...`);
await new Promise(r => setTimeout(r, waitTime));
}
}
}
throw lastError;
}
|
|
Result Pattern
Return success/failure without exceptions.
|
function divide(a, b) {
if (b === 0) {
return { ok: false, error: 'Division by zero' };
}
return { ok: true, value: a / b };
}
const result = divide(10, 0);
if (result.ok) {
console.log('Result:', result.value);
} else {
console.log('Error:', result.error);
}
|
|
Error Wrapping
Add context when re-throwing errors.
Node.js 16+ supports native error cause
|
class WrappedError extends Error {
constructor(message, cause) {
super(message);
this.name = 'WrappedError';
this.cause = cause;
Error.captureStackTrace(this, WrappedError);
}
}
async function processUserData(userId) {
try {
const user = await fetchUser(userId);
return user;
} catch (err) {
throw new WrappedError(`Failed to process user ${userId}`, err);
}
}
function modernErrorWrapping() {
try {
throw new Error('Original error');
} catch (err) {
throw new Error('Wrapped error', { cause: err });
}
}
|
|
Practical Example: Error Handler Middleware
Centralized error handling for applications.
Usage
|
function createErrorHandler() {
const handlers = new Map();
return {
register(ErrorClass, handler) {
handlers.set(ErrorClass, handler);
},
handle(err) {
for (const [ErrorClass, handler] of handlers) {
if (err instanceof ErrorClass) {
return handler(err);
}
}
console.error('Unhandled error:', err);
return {
statusCode: 500,
message: 'Internal server error'
};
}
};
}
const errorHandler = createErrorHandler();
errorHandler.register(ValidationError, (err) => ({
statusCode: err.statusCode,
message: err.message,
field: err.field
}));
errorHandler.register(NotFoundError, (err) => ({
statusCode: err.statusCode,
message: err.message,
resource: err.resource
}));
const response = errorHandler.handle(
new ValidationError('email', 'Invalid email')
);
console.log('Error response:', response);
|
|
Practical Example: Safe Async Wrapper
Wrapper for safe async execution.
Usage:
const safeReadFile = safeAsync(readFile);
const [err, content] = await safeReadFile('./file.txt', 'utf8');
if (err) console.log('Error:', err.message);
else console.log('Content:', content);
|
function safeAsync(fn) {
return async (...args) => {
try {
const result = await fn(...args);
return [null, result];
} catch (err) {
return [err, null];
}
};
}
|
|
Best Practices Summary
1. Always handle errors - never ignore them
2. Use custom error classes for different error types
3. Include useful context in error messages
4. Use Error.captureStackTrace for clean stack traces
5. Implement retry logic for transient failures
6. Log errors appropriately for debugging
7. Clean up resources in finally blocks
8. Fail fast - don't hide errors
9. Use process-level handlers as last resort
10. Test error handling paths
|
|