Error Handling Strategies
Learn comprehensive error handling strategies for production Node.js applications including error classification, recovery, and monitoring. This is a foundational concept in server-side JavaScript development that professional developers rely on daily. The explanations below are written to be beginner-friendly while covering the depth and nuance that comes from real-world Node.js experience. Take your time with each section and practice the examples
Error Classification
Classify errors into different categories to handle them appropriately and provide better user experience.. This is an essential concept that every Node.js developer must understand thoroughly. In professional development environments, getting this right can mean the difference between code that works reliably and code that breaks in production. The following sections break this down into clear, digestible pieces with practical examples you can try immediately
Custom Error Classes
// Custom error classes
class AppError extends Error {
constructor(message, statusCode, isOperational = true) {
super(message);
this.statusCode = statusCode;
this.isOperational = isOperational;
this.timestamp = new Date().toISOString();
Error.captureStackTrace(this, this.constructor);
}
}
class ValidationError extends AppError {
constructor(message, field = null) {
super(message, 400);
this.field = field;
this.type = 'ValidationError';
}
}
class NotFoundError extends AppError {
constructor(resource = 'Resource') {
super(`${resource} not found`, 404);
this.type = 'NotFoundError';
}
}
class DatabaseError extends AppError {
constructor(message, originalError = null) {
super(message, 500);
this.type = 'DatabaseError';
this.originalError = originalError;
}
}Error Recovery Strategies
// Retry mechanism with exponential backoff
class RetryHandler {
constructor(maxRetries = 3, baseDelay = 1000) {
this.maxRetries = maxRetries;
this.baseDelay = baseDelay;
}
async execute(fn, context = {}) {
let lastError;
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (attempt === this.maxRetries || !this.isRetryable(error)) {
throw error;
}
const delay = this.baseDelay * Math.pow(2, attempt - 1);
console.log(`Attempt ${attempt} failed, retrying in ${delay}ms`);
await this.sleep(delay);
}
}
throw lastError;
}
isRetryable(error) {
// Define which errors are retryable
const retryableErrors = ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND'];
return retryableErrors.includes(error.code);
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Circuit breaker pattern
class CircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.threshold = threshold;
this.timeout = timeout;
this.failureCount = 0;
this.lastFailureTime = null;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
}
async execute(fn) {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime > this.timeout) {
this.state = 'HALF_OPEN';
} else {
throw new Error('Circuit breaker is OPEN');
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failureCount = 0;
this.state = 'CLOSED';
}
onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.threshold) {
this.state = 'OPEN';
}
}
}Global Error Handling
// Global error handling middleware
const errorHandler = (err, req, res, next) => {
let error = { ...err };
error.message = err.message;
// Log error
console.error('Error:', err);
// Mongoose bad ObjectId
if (err.name === 'CastError') {
const message = 'Resource not found';
error = new NotFoundError(message);
}
// Mongoose duplicate key
if (err.code === 11000) {
const message = 'Duplicate field value entered';
error = new ValidationError(message);
}
// Mongoose validation error
if (err.name === 'ValidationError') {
const message = Object.values(err.errors).map(val => val.message).join(', ');
error = new ValidationError(message);
}
// JWT errors
if (err.name === 'JsonWebTokenError') {
const message = 'Invalid token';
error = new AppError(message, 401);
}
if (err.name === 'TokenExpiredError') {
const message = 'Token expired';
error = new AppError(message, 401);
}
res.status(error.statusCode || 500).json({
success: false,
error: error.message || 'Server Error',
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
};
// Async error wrapper
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
// Unhandled promise rejections
process.on('unhandledRejection', (err, promise) => {
console.error('Unhandled Promise Rejection:', err);
// Close server & exit process
process.exit(1);
});
// Uncaught exceptions
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:', err);
process.exit(1);
});Error Monitoring
// Error monitoring and alerting
class ErrorMonitor {
constructor() {
this.errorCounts = new Map();
this.alertThreshold = 10;
this.timeWindow = 60000; // 1 minute
}
recordError(error) {
const key = `${error.name}:${error.message}`;
const now = Date.now();
if (!this.errorCounts.has(key)) {
this.errorCounts.set(key, []);
}
const errors = this.errorCounts.get(key);
errors.push(now);
// Remove old errors outside time window
const recentErrors = errors.filter(time => now - time < this.timeWindow);
this.errorCounts.set(key, recentErrors);
// Check if threshold exceeded
if (recentErrors.length >= this.alertThreshold) {
this.sendAlert(key, recentErrors.length);
}
}
sendAlert(errorKey, count) {
console.error(`ALERT: ${errorKey} occurred ${count} times in the last minute`);
// Send to monitoring service
}
}
// Usage
const errorMonitor = new ErrorMonitor();
// In error handler
errorMonitor.recordError(error);Mini-Project: Complete Error Handling System
// Complete error handling system
const express = require('express');
const winston = require('winston');
// Error classes
class AppError extends Error {
constructor(message, statusCode, isOperational = true) {
super(message);
this.statusCode = statusCode;
this.isOperational = isOperational;
this.timestamp = new Date().toISOString();
Error.captureStackTrace(this, this.constructor);
}
}
class ValidationError extends AppError {
constructor(message, field = null) {
super(message, 400);
this.field = field;
this.type = 'ValidationError';
}
}
// Logger setup
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
// Error monitoring
class ErrorMonitor {
constructor() {
this.errorCounts = new Map();
this.alertThreshold = 5;
}
recordError(error) {
const key = error.constructor.name;
const count = this.errorCounts.get(key) || 0;
this.errorCounts.set(key, count + 1);
if (count >= this.alertThreshold) {
this.sendAlert(key, count);
}
}
sendAlert(errorType, count) {
logger.error(`ALERT: ${errorType} occurred ${count} times`);
}
}
const errorMonitor = new ErrorMonitor();
// Express app
const app = express();
// Error handling middleware
const errorHandler = (err, req, res, next) => {
let error = { ...err };
error.message = err.message;
// Log error
logger.error('Error occurred', {
error: err.message,
stack: err.stack,
url: req.url,
method: req.method
});
// Record error for monitoring
errorMonitor.recordError(err);
// Handle specific error types
if (err.name === 'ValidationError') {
error = new ValidationError('Validation failed');
}
res.status(error.statusCode || 500).json({
success: false,
error: error.message || 'Server Error',
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
};
// Async error wrapper
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
// Example route with error handling
app.get('/api/users/:id', asyncHandler(async (req, res, next) => {
const { id } = req.params;
if (!id || isNaN(id)) {
throw new ValidationError('Invalid user ID');
}
// Simulate database operation
const user = await simulateDatabaseOperation(id);
if (!user) {
throw new AppError('User not found', 404);
}
res.json({ success: true, data: user });
}));
// Simulate database operation
async function simulateDatabaseOperation(id) {
// Simulate random errors
if (Math.random() < 0.3) {
throw new Error('Database connection failed');
}
return { id, name: 'John Doe' };
}
// Global error handler
app.use(errorHandler);
// Unhandled promise rejections
process.on('unhandledRejection', (err, promise) => {
logger.error('Unhandled Promise Rejection:', err);
process.exit(1);
});
// Uncaught exceptions
process.on('uncaughtException', (err) => {
logger.error('Uncaught Exception:', err);
process.exit(1);
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
logger.info(`Server running on port ${PORT}`);
});
module.exports = app;