Back to Blog
Node.jsError HandlingAsyncBest Practices

Node.js Error Handling Patterns

Master error handling in Node.js. From try-catch to async errors to custom error classes.

B
Bootspring Team
Engineering
November 17, 2020
7 min read

Proper error handling prevents crashes and improves debugging. Here's how to handle errors effectively.

Error Types#

1// Standard Error types 2throw new Error('Something went wrong'); 3throw new TypeError('Expected a string'); 4throw new RangeError('Value out of range'); 5throw new ReferenceError('Variable not defined'); 6throw new SyntaxError('Invalid syntax'); 7 8// Error properties 9const error = new Error('Failed to process'); 10console.log(error.message); // 'Failed to process' 11console.log(error.name); // 'Error' 12console.log(error.stack); // Stack trace 13 14// Custom error with cause (ES2022) 15try { 16 JSON.parse(invalidJson); 17} catch (parseError) { 18 throw new Error('Configuration failed', { cause: parseError }); 19}

Custom Error Classes#

1// Custom error class 2class AppError extends Error { 3 constructor(message, statusCode = 500) { 4 super(message); 5 this.name = 'AppError'; 6 this.statusCode = statusCode; 7 this.isOperational = true; 8 9 Error.captureStackTrace(this, this.constructor); 10 } 11} 12 13class NotFoundError extends AppError { 14 constructor(resource = 'Resource') { 15 super(`${resource} not found`, 404); 16 this.name = 'NotFoundError'; 17 } 18} 19 20class ValidationError extends AppError { 21 constructor(errors) { 22 super('Validation failed', 400); 23 this.name = 'ValidationError'; 24 this.errors = errors; 25 } 26} 27 28class AuthenticationError extends AppError { 29 constructor(message = 'Authentication required') { 30 super(message, 401); 31 this.name = 'AuthenticationError'; 32 } 33} 34 35class AuthorizationError extends AppError { 36 constructor(message = 'Access denied') { 37 super(message, 403); 38 this.name = 'AuthorizationError'; 39 } 40} 41 42// Usage 43function getUser(id) { 44 const user = database.find(id); 45 if (!user) { 46 throw new NotFoundError('User'); 47 } 48 return user; 49}

Synchronous Error Handling#

1// try-catch 2function parseConfig(configString) { 3 try { 4 return JSON.parse(configString); 5 } catch (error) { 6 console.error('Failed to parse config:', error.message); 7 return null; 8 } 9} 10 11// Re-throwing with context 12function loadModule(name) { 13 try { 14 return require(name); 15 } catch (error) { 16 throw new Error(`Failed to load module '${name}': ${error.message}`); 17 } 18} 19 20// Finally for cleanup 21function processFile(path) { 22 let handle; 23 try { 24 handle = fs.openSync(path, 'r'); 25 // Process file 26 return data; 27 } catch (error) { 28 console.error('Error processing file:', error); 29 throw error; 30 } finally { 31 if (handle) { 32 fs.closeSync(handle); 33 } 34 } 35}

Async Error Handling#

1// Async/await with try-catch 2async function fetchUser(id) { 3 try { 4 const response = await fetch(`/api/users/${id}`); 5 if (!response.ok) { 6 throw new Error(`HTTP error: ${response.status}`); 7 } 8 return await response.json(); 9 } catch (error) { 10 console.error('Failed to fetch user:', error); 11 throw error; 12 } 13} 14 15// Handle multiple async operations 16async function processOrder(orderId) { 17 try { 18 const order = await getOrder(orderId); 19 const inventory = await checkInventory(order.items); 20 const payment = await processPayment(order); 21 await updateOrder(orderId, { status: 'completed' }); 22 return { order, payment }; 23 } catch (error) { 24 await updateOrder(orderId, { status: 'failed', error: error.message }); 25 throw error; 26 } 27} 28 29// Promise.all error handling 30async function fetchAll(urls) { 31 try { 32 return await Promise.all(urls.map(url => fetch(url))); 33 } catch (error) { 34 // One failure fails all 35 console.error('One or more fetches failed:', error); 36 throw error; 37 } 38} 39 40// Promise.allSettled for partial success 41async function fetchAllSettled(urls) { 42 const results = await Promise.allSettled(urls.map(url => fetch(url))); 43 44 const successful = results 45 .filter(r => r.status === 'fulfilled') 46 .map(r => r.value); 47 48 const failed = results 49 .filter(r => r.status === 'rejected') 50 .map(r => r.reason); 51 52 if (failed.length > 0) { 53 console.warn(`${failed.length} requests failed`); 54 } 55 56 return successful; 57}

Express Error Handling#

1const express = require('express'); 2const app = express(); 3 4// Async route handler wrapper 5const asyncHandler = (fn) => (req, res, next) => { 6 Promise.resolve(fn(req, res, next)).catch(next); 7}; 8 9// Routes 10app.get('/users/:id', asyncHandler(async (req, res) => { 11 const user = await getUser(req.params.id); 12 if (!user) { 13 throw new NotFoundError('User'); 14 } 15 res.json(user); 16})); 17 18// 404 handler 19app.use((req, res, next) => { 20 next(new NotFoundError('Route')); 21}); 22 23// Error handling middleware 24app.use((error, req, res, next) => { 25 console.error(error.stack); 26 27 // Operational errors (expected) 28 if (error.isOperational) { 29 return res.status(error.statusCode).json({ 30 status: 'error', 31 message: error.message, 32 ...(error.errors && { errors: error.errors }), 33 }); 34 } 35 36 // Programming errors (unexpected) 37 res.status(500).json({ 38 status: 'error', 39 message: 'Internal server error', 40 }); 41});

Global Error Handlers#

1// Uncaught exceptions 2process.on('uncaughtException', (error) => { 3 console.error('Uncaught Exception:', error); 4 // Log to monitoring service 5 // Perform graceful shutdown 6 process.exit(1); 7}); 8 9// Unhandled promise rejections 10process.on('unhandledRejection', (reason, promise) => { 11 console.error('Unhandled Rejection at:', promise, 'reason:', reason); 12 // In Node.js 15+, this becomes uncaughtException by default 13}); 14 15// Warning events 16process.on('warning', (warning) => { 17 console.warn('Warning:', warning.name, warning.message); 18}); 19 20// Graceful shutdown 21process.on('SIGTERM', () => { 22 console.log('SIGTERM received, shutting down gracefully'); 23 server.close(() => { 24 console.log('Server closed'); 25 process.exit(0); 26 }); 27});

Error Logging#

1// Structured error logging 2class Logger { 3 error(error, context = {}) { 4 const logEntry = { 5 timestamp: new Date().toISOString(), 6 level: 'error', 7 message: error.message, 8 name: error.name, 9 stack: error.stack, 10 ...context, 11 }; 12 13 if (error.cause) { 14 logEntry.cause = { 15 message: error.cause.message, 16 stack: error.cause.stack, 17 }; 18 } 19 20 console.error(JSON.stringify(logEntry)); 21 22 // Send to monitoring service 23 this.sendToMonitoring(logEntry); 24 } 25 26 sendToMonitoring(logEntry) { 27 // Implementation 28 } 29} 30 31const logger = new Logger(); 32 33// Usage 34try { 35 await riskyOperation(); 36} catch (error) { 37 logger.error(error, { 38 userId: req.user?.id, 39 requestId: req.id, 40 path: req.path, 41 }); 42 throw error; 43}

Error Recovery#

1// Retry with exponential backoff 2async function retry(fn, options = {}) { 3 const { 4 maxAttempts = 3, 5 initialDelay = 1000, 6 maxDelay = 10000, 7 factor = 2, 8 } = options; 9 10 let attempt = 0; 11 let delay = initialDelay; 12 13 while (attempt < maxAttempts) { 14 try { 15 return await fn(); 16 } catch (error) { 17 attempt++; 18 19 if (attempt >= maxAttempts) { 20 throw error; 21 } 22 23 console.log(`Attempt ${attempt} failed, retrying in ${delay}ms`); 24 await new Promise(resolve => setTimeout(resolve, delay)); 25 26 delay = Math.min(delay * factor, maxDelay); 27 } 28 } 29} 30 31// Usage 32const result = await retry(() => fetchData(), { 33 maxAttempts: 5, 34 initialDelay: 500, 35}); 36 37// Circuit breaker pattern 38class CircuitBreaker { 39 constructor(fn, options = {}) { 40 this.fn = fn; 41 this.failureThreshold = options.failureThreshold || 5; 42 this.resetTimeout = options.resetTimeout || 30000; 43 this.failures = 0; 44 this.state = 'CLOSED'; 45 this.nextAttempt = null; 46 } 47 48 async execute(...args) { 49 if (this.state === 'OPEN') { 50 if (Date.now() < this.nextAttempt) { 51 throw new Error('Circuit breaker is OPEN'); 52 } 53 this.state = 'HALF-OPEN'; 54 } 55 56 try { 57 const result = await this.fn(...args); 58 this.onSuccess(); 59 return result; 60 } catch (error) { 61 this.onFailure(); 62 throw error; 63 } 64 } 65 66 onSuccess() { 67 this.failures = 0; 68 this.state = 'CLOSED'; 69 } 70 71 onFailure() { 72 this.failures++; 73 if (this.failures >= this.failureThreshold) { 74 this.state = 'OPEN'; 75 this.nextAttempt = Date.now() + this.resetTimeout; 76 } 77 } 78}

Validation Errors#

1// Aggregate validation errors 2function validate(data, schema) { 3 const errors = []; 4 5 for (const [field, rules] of Object.entries(schema)) { 6 const value = data[field]; 7 8 if (rules.required && (value === undefined || value === '')) { 9 errors.push({ field, message: `${field} is required` }); 10 continue; 11 } 12 13 if (rules.type && typeof value !== rules.type) { 14 errors.push({ field, message: `${field} must be a ${rules.type}` }); 15 } 16 17 if (rules.min !== undefined && value < rules.min) { 18 errors.push({ field, message: `${field} must be at least ${rules.min}` }); 19 } 20 21 if (rules.pattern && !rules.pattern.test(value)) { 22 errors.push({ field, message: rules.patternMessage || `${field} is invalid` }); 23 } 24 } 25 26 if (errors.length > 0) { 27 throw new ValidationError(errors); 28 } 29 30 return true; 31} 32 33// Usage 34try { 35 validate(req.body, { 36 email: { required: true, pattern: /^\S+@\S+$/, patternMessage: 'Invalid email' }, 37 age: { type: 'number', min: 18 }, 38 }); 39} catch (error) { 40 if (error instanceof ValidationError) { 41 return res.status(400).json({ errors: error.errors }); 42 } 43 throw error; 44}

Best Practices#

Error Design: ✓ Use custom error classes ✓ Include error codes ✓ Preserve error chain with cause ✓ Distinguish operational vs programming errors Handling: ✓ Catch at appropriate boundaries ✓ Don't swallow errors silently ✓ Log with context ✓ Fail fast for programming errors Recovery: ✓ Implement retries for transient failures ✓ Use circuit breakers ✓ Degrade gracefully ✓ Have fallback strategies Monitoring: ✓ Log structured errors ✓ Track error rates ✓ Alert on anomalies ✓ Include request context

Conclusion#

Effective error handling requires custom error classes, proper async handling, and comprehensive logging. Use global handlers for uncaught errors, implement retry and circuit breaker patterns for resilience, and always log with sufficient context for debugging.

Share this article

Help spread the word about Bootspring