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.