Difficulty: Intermediate
Error handling is one of the most critical aspects of building reliable Express applications. When something goes wrong - a database query fails, a required field is missing, or an external API is down - your application must handle the error gracefully instead of crashing or leaking implementation details to the client. Express provides a specific pattern for centralized error handling that keeps your route handlers clean and your error responses consistent.
Express distinguishes between regular middleware (3 parameters: req, res, next) and error-handling middleware (4 parameters: err, req, res, next). When you call `next(err)` with an argument, Express skips all remaining regular middleware and jumps directly to the first error-handling middleware it finds. This means you can define a single error handler at the end of your middleware stack that catches all errors from all routes.
The error middleware should be the last middleware registered with app.use(). It receives the error object, inspects it, and sends an appropriate response. In development, you might include the error stack trace in the response for debugging. In production, you should send a generic message to the client while logging the full error details server-side. Never expose stack traces or internal error details to end users - they reveal your code structure to potential attackers.
Handling errors in async route handlers requires special attention. Express 4 does not automatically catch errors thrown in async functions or rejected promises. If an async handler throws without a try/catch, the error is silently swallowed, and the request hangs until it times out. The solution is an async wrapper function that catches rejected promises and forwards them to next(). Express 5 handles this natively, but the wrapper pattern is still widely used and is good to understand.
A well-structured Express application has error handling at multiple levels: input validation errors (400), authentication errors (401), authorization errors (403), not-found errors (404), and server errors (500). Each type has its own HTTP status code and message format. The centralized error handler inspects the error type and sends the appropriate response.
const express = require('express');
const app = express();
app.use(express.json());
// Routes that might throw errors
app.get('/api/users/:id', (req, res, next) => {
try {
const id = parseInt(req.params.id);
if (isNaN(id)) {
const error = new Error('Invalid user ID');
error.statusCode = 400;
throw error;
}
// Simulate user not found
const error = new Error('User not found');
error.statusCode = 404;
throw error;
} catch (err) {
next(err); // Forward to error handler
}
});
// 404 handler for unmatched routes
app.use((req, res, next) => {
const error = new Error(`Route not found: ${req.method} ${req.path}`);
error.statusCode = 404;
next(error);
});
// Centralized error handler (4 parameters!)
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
const message = statusCode === 500 ? 'Internal server error' : err.message;
console.error(`[${statusCode}] ${err.message}`);
if (statusCode === 500) console.error(err.stack);
res.status(statusCode).json({
error: message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
});
console.log('Error handling middleware configured');
console.log('Handles: validation, 404, and server errors');
The route handler catches errors and forwards them with next(err). The 404 middleware catches unmatched routes. The error handler (4 params) sends appropriate responses - hiding stack traces in production and including them in development.
const express = require('express');
const app = express();
// Async wrapper - catches rejected promises and forwards to error handler
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
// Without wrapper - error is swallowed, request hangs
// app.get('/bad', async (req, res) => {
// throw new Error('This error is lost!');
// });
// With wrapper - error is properly forwarded
app.get('/api/data', asyncHandler(async (req, res) => {
// Simulate async operation that fails
const data = await fetchFromDatabase(); // throws
res.json(data);
}));
app.get('/api/items', asyncHandler(async (req, res) => {
const items = await getItems();
if (items.length === 0) {
const error = new Error('No items found');
error.statusCode = 404;
throw error; // Caught by asyncHandler
}
res.json(items);
}));
// Error handler
app.use((err, req, res, next) => {
res.status(err.statusCode || 500).json({ error: err.message });
});
console.log('asyncHandler wrapper defined');
console.log('Wraps async routes to catch rejected promises');
The asyncHandler function wraps an async route handler and catches any rejected promise, forwarding the error to Express via next(). This eliminates repetitive try/catch blocks in every route. Express 5 handles this natively, but this pattern is essential for Express 4.
const express = require('express');
const app = express();
app.use(express.json());
function asyncHandler(fn) {
return (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
}
// Validation error
app.post('/api/users', asyncHandler(async (req, res) => {
const { email, name } = req.body;
if (!email || !name) {
const err = new Error('Email and name are required');
err.statusCode = 400;
throw err;
}
res.status(201).json({ email, name });
}));
// Resource not found
app.get('/api/users/:id', asyncHandler(async (req, res) => {
const user = null; // Simulate DB lookup returning null
if (!user) {
const err = new Error('User not found');
err.statusCode = 404;
throw err;
}
res.json(user);
}));
// Database error (simulated)
app.get('/api/reports', asyncHandler(async (req, res) => {
// This simulates a DB connection error
throw new Error('Connection refused');
}));
// Error handler with error classification
app.use((err, req, res, next) => {
// Log all errors
console.error(`[ERROR] ${req.method} ${req.path}: ${err.message}`);
const statusCode = err.statusCode || 500;
res.status(statusCode).json({
error: statusCode < 500 ? err.message : 'Something went wrong',
status: statusCode
});
});
console.log('Routes with various error scenarios configured');
This shows three common error patterns: validation errors (400), not-found errors (404), and unexpected server errors (500). The centralized error handler distinguishes between client errors (4xx, show the message) and server errors (5xx, hide the details).
error middleware, next(err), async error wrapper, error propagation, centralized error handling