Error Handling Patterns
Proper error handling makes your application reliable and helps debug issues quickly. Express provides several ways to handle errors gracefully.
Basic Error Handling
Try-Catch for Synchronous Errors
const express = require('express');
const app = express();
app.get('/api/users/:id', (req, res) => {
try {
const id = parseInt(req.params.id);
if (isNaN(id)) {
throw new Error('Invalid user ID');
}
// Simulate error
if (id > 100) {
throw new Error('User not found');
}
res.json({ id, name: 'User' });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
Problems with Basic Approach
// ❌ Repetitive error handling
app.get('/route1', (req, res) => {
try {
// ... logic
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.get('/route2', (req, res) => {
try {
// ... logic
} catch (error) {
res.status(500).json({ error: error.message });
}
});
Express Error Handler Middleware
Centralized error handling with 4-parameter middleware:
const express = require('express');
const app = express();
app.use(express.json());
// Routes
app.get('/api/users/:id', (req, res, next) => {
const id = parseInt(req.params.id);
if (isNaN(id)) {
const error = new Error('Invalid ID');
error.status = 400;
return next(error); // Pass to error handler
}
res.json({ id });
});
// Error handler middleware (must have 4 parameters!)
app.use((err, req, res, next) => {
console.error('Error:', err.message);
const statusCode = err.status || 500;
const message = err.message || 'Internal Server Error';
res.status(statusCode).json({
error: message
});
});
app.listen(3000);
Key points:
- Error handler must have 4 parameters:
(err, req, res, next) - Must be defined after all routes
- Use
next(error)to pass errors to handler
Custom Error Classes
Create specific error types:
// errors/AppError.js
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = AppError;
Specific error types:
class NotFoundError extends AppError {
constructor(message = 'Resource not found') {
super(message, 404);
}
}
class ValidationError extends AppError {
constructor(message = 'Validation failed') {
super(message, 400);
}
}
class UnauthorizedError extends AppError {
constructor(message = 'Unauthorized') {
super(message, 401);
}
}
module.exports = { AppError, NotFoundError, ValidationError, UnauthorizedError };
Usage:
const { NotFoundError, ValidationError } = require('./errors/AppError');
app.get('/api/users/:id', (req, res, next) => {
const id = parseInt(req.params.id);
if (isNaN(id)) {
return next(new ValidationError('Invalid user ID'));
}
const user = users.find(u => u.id === id);
if (!user) {
return next(new NotFoundError('User not found'));
}
res.json(user);
});
// Error handler
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
res.status(statusCode).json({
error: err.message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
});
Async Error Handling
Problem with Async/Await
// ❌ Unhandled promise rejection!
app.get('/api/data', async (req, res) => {
const data = await fetchData(); // If this throws, Express won't catch it
res.json(data);
});
Solution 1: Try-Catch
app.get('/api/data', async (req, res, next) => {
try {
const data = await fetchData();
res.json(data);
} catch (error) {
next(error);
}
});
Solution 2: Async Handler Wrapper
// utils/asyncHandler.js
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
module.exports = asyncHandler;
Usage:
const asyncHandler = require('./utils/asyncHandler');
app.get('/api/data', asyncHandler(async (req, res) => {
const data = await fetchData(); // Errors automatically caught
res.json(data);
}));
app.post('/api/users', asyncHandler(async (req, res) => {
const user = await createUser(req.body);
res.status(201).json(user);
}));
Express 5 (Future)
Express 5+ will handle async errors automatically:
// No wrapper needed in Express 5+
app.get('/api/data', async (req, res) => {
const data = await fetchData();
res.json(data);
});
Complete Error Handler
Production-ready error handling:
// middleware/errorHandler.js
function errorHandler(err, req, res, next) {
// Log error
console.error('Error occurred:', {
message: err.message,
stack: err.stack,
url: req.url,
method: req.method
});
// Default to 500
let statusCode = err.statusCode || 500;
let message = err.message;
// Mongoose validation error
if (err.name === 'ValidationError') {
statusCode = 400;
message = Object.values(err.errors).map(e => e.message).join(', ');
}
// Mongoose cast error (invalid ObjectId)
if (err.name === 'CastError') {
statusCode = 400;
message = 'Invalid ID format';
}
// MongoDB duplicate key
if (err.code === 11000) {
statusCode = 400;
message = 'Duplicate value entered';
}
// JWT errors
if (err.name === 'JsonWebTokenError') {
statusCode = 401;
message = 'Invalid token';
}
if (err.name === 'TokenExpiredError') {
statusCode = 401;
message = 'Token expired';
}
// Response
res.status(statusCode).json({
success: false,
error: message,
// Show stack trace in development only
...(process.env.NODE_ENV === 'development' && {
stack: err.stack,
originalError: err
})
});
}
module.exports = errorHandler;
404 Handler
Handle routes that don’t exist:
const express = require('express');
const app = express();
// Routes
app.get('/api/users', (req, res) => {
res.json({ users: [] });
});
// 404 handler (after all routes)
app.use((req, res, next) => {
res.status(404).json({
error: 'Route not found',
path: req.url
});
});
// Or pass to error handler
app.use((req, res, next) => {
const error = new Error('Route not found');
error.status = 404;
next(error);
});
// Error handler
app.use((err, req, res, next) => {
res.status(err.status || 500).json({
error: err.message
});
});
Validation Errors
Handle validation errors consistently:
const { ValidationError } = require('./errors/AppError');
// Validation middleware
function validateUser(req, res, next) {
const { name, email } = req.body;
const errors = [];
if (!name || name.trim().length < 2) {
errors.push('Name must be at least 2 characters');
}
if (!email || !email.includes('@')) {
errors.push('Valid email is required');
}
if (errors.length > 0) {
return next(new ValidationError(errors.join(', ')));
}
next();
}
app.post('/api/users', validateUser, asyncHandler(async (req, res) => {
const user = await createUser(req.body);
res.status(201).json(user);
}));
Error Response Formats
Development Response
{
"success": false,
"error": "User not found",
"statusCode": 404,
"stack": "Error: User not found\n at /app/routes/users.js:15:11...",
"timestamp": "2024-01-15T10:30:00.000Z",
"path": "/api/users/999",
"method": "GET"
}
Production Response
{
"success": false,
"error": "User not found",
"statusCode": 404
}
Implementation:
function errorHandler(err, req, res, next) {
const isDevelopment = process.env.NODE_ENV === 'development';
const response = {
success: false,
error: err.message,
statusCode: err.statusCode || 500
};
if (isDevelopment) {
response.stack = err.stack;
response.timestamp = new Date().toISOString();
response.path = req.url;
response.method = req.method;
}
res.status(response.statusCode).json(response);
}
Database Error Handling
Example with MongoDB/Mongoose:
const asyncHandler = require('./utils/asyncHandler');
const { NotFoundError } = require('./errors/AppError');
app.get('/api/users/:id', asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
throw new NotFoundError('User not found');
}
res.json(user);
}));
app.post('/api/users', asyncHandler(async (req, res) => {
const user = await User.create(req.body);
res.status(201).json(user);
}));
// Error handler catches Mongoose errors
app.use((err, req, res, next) => {
let error = { ...err };
error.message = err.message;
// Mongoose validation error
if (err.name === 'ValidationError') {
const message = Object.values(err.errors).map(e => e.message);
error = new AppError(message, 400);
}
// Mongoose duplicate key
if (err.code === 11000) {
error = new AppError('Duplicate field value', 400);
}
// Mongoose cast error
if (err.name === 'CastError') {
error = new AppError('Invalid ID', 400);
}
res.status(error.statusCode || 500).json({
error: error.message || 'Server Error'
});
});
Complete Example
Full application with error handling:
const express = require('express');
const asyncHandler = require('./utils/asyncHandler');
const { AppError, NotFoundError, ValidationError } = require('./errors/AppError');
const app = express();
app.use(express.json());
// Mock database
const users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' }
];
// Routes
app.get('/api/users', asyncHandler(async (req, res) => {
// Simulate async operation
const result = await Promise.resolve(users);
res.json(result);
}));
app.get('/api/users/:id', asyncHandler(async (req, res) => {
const id = parseInt(req.params.id);
if (isNaN(id)) {
throw new ValidationError('Invalid user ID');
}
const user = users.find(u => u.id === id);
if (!user) {
throw new NotFoundError('User not found');
}
res.json(user);
}));
app.post('/api/users', asyncHandler(async (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
throw new ValidationError('Name and email are required');
}
const newUser = {
id: users.length + 1,
name,
email
};
users.push(newUser);
res.status(201).json(newUser);
}));
// Simulate error
app.get('/api/error', (req, res) => {
throw new Error('Something went wrong!');
});
// 404 handler
app.use((req, res, next) => {
next(new NotFoundError('Route not found'));
});
// Error handler
app.use((err, req, res, next) => {
console.error('Error:', err.message);
const statusCode = err.statusCode || 500;
const isDevelopment = process.env.NODE_ENV === 'development';
res.status(statusCode).json({
success: false,
error: err.message,
...(isDevelopment && { stack: err.stack })
});
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Best Practices
✅ DO:
- Use centralized error handler middleware
- Create custom error classes
- Use async handler wrapper
- Log errors for debugging
- Return appropriate status codes
- Hide sensitive error details in production
- Validate input early
❌ DON’T:
- Expose stack traces in production
- Forget to call
next(error) - Use try-catch everywhere (use wrapper)
- Return 200 for errors
- Ignore unhandled promise rejections
- Expose internal error messages to users
Summary
| Pattern | Use Case |
|---|---|
| Error handler middleware | Centralized error handling |
| Custom error classes | Specific error types |
| Async handler wrapper | Simplify async error handling |
| Try-catch | Synchronous error handling |
next(error) |
Pass errors to handler |
| 404 handler | Handle unknown routes |
Error Handler Signature:
app.use((err, req, res, next) => {
// Must have 4 parameters!
});
Async Handler Pattern:
const asyncHandler = fn => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
Next Article: Check out the Browser tutorials to continue learning!