Error Handling
Implement proper error handling and validation in Express.js applications. 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 Handling in Express.js
Error handling is crucial for building robust Express.js applications. Express.js provides built-in error handling mechanisms and allows you to create custom error handlers for different scenarios.. 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
Built-in Error Handling
const express = require('express');
const app = express();
// Error handling middleware (must be last)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Something went wrong!' });
});
// Async error handling
app.get('/async', async (req, res, next) => {
try {
const result = await someAsyncOperation();
res.json(result);
} catch (error) {
next(error); // Pass error to error handling middleware
}
});
// Express 5.0+ async error handling
app.get('/async-v5', async (req, res) => {
const result = await someAsyncOperation();
res.json(result);
// Errors are automatically caught and passed to error middleware
});
// Custom error class
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
// Using custom error class
app.get('/users/:id', (req, res, next) => {
const userId = req.params.id;
if (!userId || isNaN(userId)) {
return next(new AppError('Invalid user ID', 400));
}
// Process user logic
res.json({ message: 'User found', id: userId });
});
// Error handling middleware for custom errors
app.use((err, req, res, next) => {
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
if (process.env.NODE_ENV === 'development') {
res.status(err.statusCode).json({
status: err.status,
error: err,
message: err.message,
stack: err.stack
});
} else {
// Production error response
if (err.isOperational) {
res.status(err.statusCode).json({
status: err.status,
message: err.message
});
} else {
// Programming or unknown errors
console.error('ERROR 💥', err);
res.status(500).json({
status: 'error',
message: 'Something went wrong!'
});
}
}
});Input Validation
const express = require('express');
const app = express();
app.use(express.json());
// Manual validation
app.post('/users', (req, res, next) => {
const { name, email, age } = req.body;
// Validation
if (!name || name.trim().length < 2) {
return res.status(400).json({ error: 'Name must be at least 2 characters long' });
}
if (!email || !email.includes('@')) {
return res.status(400).json({ error: 'Valid email is required' });
}
if (age && (age < 0 || age > 120)) {
return res.status(400).json({ error: 'Age must be between 0 and 120' });
}
// Process valid data
res.status(201).json({ message: 'User created successfully' });
});
// Using validation libraries
const { body, validationResult } = require('express-validator');
app.post('/users-validated', [
body('name').isLength({ min: 2 }).withMessage('Name must be at least 2 characters'),
body('email').isEmail().withMessage('Must be a valid email'),
body('age').optional().isInt({ min: 0, max: 120 }).withMessage('Age must be between 0 and 120')
], (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
res.status(201).json({ message: 'User created successfully' });
});
// Sanitization
const { body, sanitizeBody } = require('express-validator');
app.post('/users-sanitized', [
body('name').trim().escape(),
body('email').normalizeEmail(),
sanitizeBody('age').toInt()
], (req, res) => {
// Data is now sanitized
res.status(201).json({ message: 'User created', data: req.body });
});Advanced Error Handling Patterns
// Advanced error handling patterns for Express.js
const express = require('express');
const app = express();
// Custom error classes
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
class ValidationError extends AppError {
constructor(message, details = []) {
super(message, 400);
this.details = details;
}
}
class NotFoundError extends AppError {
constructor(resource = 'Resource') {
super(`${resource} not found`, 404);
}
}
// 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 AppError(message, 404);
}
// Mongoose duplicate key
if (err.code === 11000) {
const message = 'Duplicate field value entered';
error = new AppError(message, 400);
}
// 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 }),
...(error.details && { details: error.details })
});
};
// Async error wrapper
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
// Usage examples
app.get('/users/:id', asyncHandler(async (req, res, next) => {
const user = await User.findById(req.params.id);
if (!user) {
return next(new NotFoundError('User'));
}
res.json({ success: true, data: user });
}));
app.post('/users', asyncHandler(async (req, res, next) => {
const { name, email } = req.body;
if (!name || !email) {
return next(new ValidationError('Name and email are required'));
}
const user = await User.create({ name, email });
res.status(201).json({ success: true, data: user });
}));
// Global error handler
app.use(errorHandler);
module.exports = app;Error Handling Best Practices
- Use custom error classes: Create specific error types for different scenarios
- Implement global error handler: Centralize error handling logic
- Log errors properly: Use structured logging with appropriate levels
- Don't expose sensitive information: Sanitize error messages in production
- Use appropriate HTTP status codes: Match status codes to error types
- Handle async errors: Use try-catch or async error wrappers
- Implement error monitoring: Use services like Sentry for production
- Create error recovery strategies: Implement retry logic where appropriate
Mini-Project: comprehensive Error Handling System
// Complete error handling system for Express.js application
const express = require('express');
const winston = require('winston');
const Sentry = require('@sentry/node');
const app = express();
// Configure logging
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' }),
new winston.transports.Console({
format: winston.format.simple()
})
]
});
// Initialize Sentry for error monitoring
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV || 'development'
});
// 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 AuthenticationError extends AppError {
constructor(message = 'Authentication failed') {
super(message, 401);
this.type = 'AuthenticationError';
}
}
class AuthorizationError extends AppError {
constructor(message = 'Insufficient permissions') {
super(message, 403);
this.type = 'AuthorizationError';
}
}
class NotFoundError extends AppError {
constructor(resource = 'Resource') {
super(`${resource} not found`, 404);
this.type = 'NotFoundError';
}
}
class ConflictError extends AppError {
constructor(message = 'Resource conflict') {
super(message, 409);
this.type = 'ConflictError';
}
}
class RateLimitError extends AppError {
constructor(message = 'Too many requests') {
super(message, 429);
this.type = 'RateLimitError';
}
}
// Error handling utilities
const handleAsync = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
const handleValidation = (schema) => (req, res, next) => {
const { error } = schema.validate(req.body);
if (error) {
const details = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message
}));
return next(new ValidationError('Validation failed', details));
}
next();
};
// Error response formatter
const formatErrorResponse = (err, req) => {
const response = {
success: false,
error: {
message: err.message,
type: err.type || 'UnknownError',
timestamp: err.timestamp || new Date().toISOString(),
requestId: req.requestId || 'unknown'
}
};
// Add additional details in development
if (process.env.NODE_ENV === 'development') {
response.error.stack = err.stack;
response.error.details = err.details;
}
return response;
};
// Global error handler
const errorHandler = (err, req, res, next) => {
let error = { ...err };
error.message = err.message;
// Log error
logger.error({
message: err.message,
stack: err.stack,
requestId: req.requestId,
url: req.url,
method: req.method,
ip: req.ip,
userAgent: req.get('User-Agent')
});
// Send to Sentry
Sentry.captureException(err);
// Handle specific error types
if (err.name === 'CastError') {
error = new NotFoundError('Resource');
}
if (err.code === 11000) {
const field = Object.keys(err.keyValue)[0];
error = new ConflictError(`${field} already exists`);
}
if (err.name === 'ValidationError') {
const details = Object.values(err.errors).map(val => ({
field: val.path,
message: val.message
}));
error = new ValidationError('Validation failed', details);
}
if (err.name === 'JsonWebTokenError') {
error = new AuthenticationError('Invalid token');
}
if (err.name === 'TokenExpiredError') {
error = new AuthenticationError('Token expired');
}
// Send error response
const statusCode = error.statusCode || 500;
const response = formatErrorResponse(error, req);
res.status(statusCode).json(response);
};
// 404 handler
const notFoundHandler = (req, res, next) => {
const error = new NotFoundError(`Route ${req.originalUrl}`);
next(error);
};
// Rate limiting error handler
const rateLimitHandler = (req, res) => {
const error = new RateLimitError('Too many requests, please try again later');
const response = formatErrorResponse(error, req);
res.status(429).json(response);
};
// Apply middleware
app.use(Sentry.requestHandler());
app.use(express.json());
// Request ID middleware
app.use((req, res, next) => {
req.requestId = Math.random().toString(36).substr(2, 9);
res.setHeader('X-Request-ID', req.requestId);
next();
});
// Example routes with error handling
app.get('/users/:id', handleAsync(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('findUser', id);
if (!user) {
throw new NotFoundError('User');
}
res.json({ success: true, data: user });
}));
app.post('/users', handleValidation(userSchema), handleAsync(async (req, res, next) => {
const { name, email } = req.body;
// Simulate database operation
const existingUser = await simulateDatabaseOperation('findUserByEmail', email);
if (existingUser) {
throw new ConflictError('User with this email already exists');
}
const user = await simulateDatabaseOperation('createUser', { name, email });
res.status(201).json({ success: true, data: user });
}));
// Simulate database operations
async function simulateDatabaseOperation(operation, data) {
// Simulate async operation
await new Promise(resolve => setTimeout(resolve, 100));
switch (operation) {
case 'findUser':
return data === '1' ? { id: 1, name: 'John Doe', email: 'john@example.com' } : null;
case 'findUserByEmail':
return data === 'john@example.com' ? { id: 1, name: 'John Doe', email: 'john@example.com' } : null;
case 'createUser':
return { id: Date.now(), ...data, createdAt: new Date() };
default:
return null;
}
}
// User validation schema (using Joi)
const Joi = require('joi');
const userSchema = Joi.object({
name: Joi.string().min(2).max(50).required(),
email: Joi.string().email().required(),
age: Joi.number().integer().min(0).max(150).optional()
});
// Apply error handlers
app.use(notFoundHandler);
app.use(errorHandler);
app.use(Sentry.errorHandler());
// Graceful shutdown
process.on('SIGTERM', () => {
logger.info('SIGTERM received, shutting down gracefully');
process.exit(0);
});
process.on('SIGINT', () => {
logger.info('SIGINT received, shutting down gracefully');
process.exit(0);
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
logger.info(`Server running on port ${PORT}`);
});
module.exports = app;