Error Handling
Overview
Robust error handling is crucial for Node.js applications. This chapter covers error types, handling strategies, logging, monitoring, and recovery patterns to build resilient applications.
Error Types and Patterns
Basic Error Handling
javascript
// basic-error-handling.js
// Synchronous error handling
function divideNumbers(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('Both arguments must be numbers');
}
if (b === 0) {
throw new Error('Division by zero is not allowed');
}
return a / b;
}
// Try-catch for synchronous operations
try {
const result = divideNumbers(10, 2);
console.log('Result:', result);
} catch (error) {
console.error('Error:', error.message);
}
// Asynchronous error handling with callbacks
function readFileCallback(filename, callback) {
const fs = require('fs');
fs.readFile(filename, 'utf8', (error, data) => {
if (error) {
return callback(error, null);
}
callback(null, data);
});
}
// Promise-based error handling
function readFilePromise(filename) {
const fs = require('fs').promises;
return fs.readFile(filename, 'utf8')
.catch(error => {
throw new Error(`Failed to read file ${filename}: ${error.message}`);
});
}
// Async/await error handling
async function processFile(filename) {
try {
const data = await readFilePromise(filename);
return data.toUpperCase();
} catch (error) {
console.error('File processing error:', error.message);
throw error;
}
}Custom Error Classes
javascript
// custom-errors.js
// Base application error
class AppError extends Error {
constructor(message, statusCode = 500, isOperational = true) {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode;
this.isOperational = isOperational;
Error.captureStackTrace(this, this.constructor);
}
}
// Specific error types
class ValidationError extends AppError {
constructor(message, field = null) {
super(message, 400);
this.field = field;
}
}
class NotFoundError extends AppError {
constructor(resource = 'Resource') {
super(`${resource} not found`, 404);
}
}
class UnauthorizedError extends AppError {
constructor(message = 'Unauthorized access') {
super(message, 401);
}
}
class DatabaseError extends AppError {
constructor(message, originalError = null) {
super(message, 500);
this.originalError = originalError;
}
}
// Usage examples
function validateUser(userData) {
if (!userData.email) {
throw new ValidationError('Email is required', 'email');
}
if (!userData.email.includes('@')) {
throw new ValidationError('Invalid email format', 'email');
}
return true;
}
function findUser(id) {
// Simulate database lookup
if (id !== '1') {
throw new NotFoundError('User');
}
return { id: '1', name: 'John Doe' };
}
module.exports = {
AppError,
ValidationError,
NotFoundError,
UnauthorizedError,
DatabaseError
};Express Error Handling
Centralized Error Handler
javascript
// express-error-handler.js
const express = require('express');
const { AppError } = require('./custom-errors');
const app = express();
app.use(express.json());
// Async wrapper to catch errors
const asyncHandler = (fn) => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
// Sample routes with errors
app.get('/error/sync', (req, res, next) => {
throw new Error('Synchronous error');
});
app.get('/error/async', asyncHandler(async (req, res, next) => {
throw new Error('Asynchronous error');
}));
app.get('/error/validation', (req, res, next) => {
const error = new AppError('Validation failed', 400);
next(error);
});
// 404 handler (must be before error handler)
app.use('*', (req, res, next) => {
const error = new AppError(`Route ${req.originalUrl} not found`, 404);
next(error);
});
// Global error handler (must be last)
app.use((error, req, res, next) => {
// Set default error properties
error.statusCode = error.statusCode || 500;
error.status = error.status || 'error';
// Log error
console.error('Error occurred:', {
message: error.message,
stack: error.stack,
url: req.originalUrl,
method: req.method,
ip: req.ip,
userAgent: req.get('User-Agent')
});
// Send error response
if (process.env.NODE_ENV === 'development') {
res.status(error.statusCode).json({
status: error.status,
error: error,
message: error.message,
stack: error.stack
});
} else {
// Production error response
if (error.isOperational) {
res.status(error.statusCode).json({
status: error.status,
message: error.message
});
} else {
// Programming error - don't leak details
res.status(500).json({
status: 'error',
message: 'Something went wrong'
});
}
}
});
app.listen(3000);Logging and Monitoring
Advanced Logging System
javascript
// logging-system.js
const winston = require('winston');
const path = require('path');
class Logger {
constructor(options = {}) {
this.logger = this.createLogger(options);
}
createLogger(options) {
const logFormat = winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.printf(({ timestamp, level, message, stack, ...meta }) => {
let log = `${timestamp} [${level.toUpperCase()}]: ${message}`;
if (Object.keys(meta).length > 0) {
log += ` ${JSON.stringify(meta)}`;
}
if (stack) {
log += `\n${stack}`;
}
return log;
})
);
const transports = [
new winston.transports.File({
filename: path.join('logs', 'error.log'),
level: 'error',
maxsize: 5242880, // 5MB
maxFiles: 5
}),
new winston.transports.File({
filename: path.join('logs', 'combined.log'),
maxsize: 5242880,
maxFiles: 5
})
];
if (process.env.NODE_ENV !== 'production') {
transports.push(
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
})
);
}
return winston.createLogger({
level: options.level || 'info',
format: logFormat,
transports
});
}
info(message, meta = {}) {
this.logger.info(message, meta);
}
error(message, error = null, meta = {}) {
const logData = { ...meta };
if (error) {
logData.error = {
message: error.message,
stack: error.stack,
name: error.name
};
}
this.logger.error(message, logData);
}
warn(message, meta = {}) {
this.logger.warn(message, meta);
}
debug(message, meta = {}) {
this.logger.debug(message, meta);
}
}
module.exports = Logger;Recovery and Resilience Patterns
Circuit Breaker Implementation
javascript
// circuit-breaker.js
class CircuitBreaker {
constructor(fn, options = {}) {
this.fn = fn;
this.failureThreshold = options.failureThreshold || 5;
this.resetTimeout = options.resetTimeout || 60000;
this.monitoringPeriod = options.monitoringPeriod || 10000;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
this.failureCount = 0;
this.lastFailureTime = null;
this.successCount = 0;
this.stats = {
totalRequests: 0,
successfulRequests: 0,
failedRequests: 0,
rejectedRequests: 0
};
}
async call(...args) {
this.stats.totalRequests++;
if (this.state === 'OPEN') {
if (this.shouldAttemptReset()) {
this.state = 'HALF_OPEN';
this.successCount = 0;
} else {
this.stats.rejectedRequests++;
throw new Error('Circuit breaker is OPEN');
}
}
try {
const result = await this.fn(...args);
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.stats.successfulRequests++;
this.failureCount = 0;
if (this.state === 'HALF_OPEN') {
this.successCount++;
if (this.successCount >= 3) {
this.state = 'CLOSED';
}
}
}
onFailure() {
this.stats.failedRequests++;
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) {
this.state = 'OPEN';
}
}
shouldAttemptReset() {
return Date.now() - this.lastFailureTime >= this.resetTimeout;
}
getStats() {
return {
...this.stats,
state: this.state,
failureCount: this.failureCount,
successRate: this.stats.totalRequests > 0
? (this.stats.successfulRequests / this.stats.totalRequests * 100).toFixed(2) + '%'
: '0%'
};
}
reset() {
this.state = 'CLOSED';
this.failureCount = 0;
this.successCount = 0;
this.lastFailureTime = null;
}
}
module.exports = CircuitBreaker;Next Steps
In the next chapter, we'll explore file handling operations including reading, writing, streaming, and file system management.
Key Takeaways
- Proper error handling prevents application crashes
- Custom error classes provide better error categorization
- Centralized error handlers improve maintainability
- Logging helps with debugging and monitoring
- Circuit breakers prevent cascading failures
- Recovery patterns improve application resilience