Centralized Error Handling
In Express, a 4-argument middleware — (err, req, res, next) — is the error handler. Place it after all routes and it catches any error forwarded by next(err), any error thrown inside a synchronous handler, and (with an asyncHandler wrapper) any rejected Promise from async handlers. One function receives every unhandled error, classifies it, logs it, and sends a consistent response. This page builds that handler and shows how to wire it into a real app.
The 4-argument signature
// Express identifies an error handler by EXACTLY 4 arguments:
app.use((err, req, res, next) => {
// ...
})
// ❌ Three arguments — this is a regular middleware, not an error handler:
app.use((req, res, next) => { /* never called for errors */ })A production-ready error handler
middleware/errorHandler.js
export function errorHandler(err, req, res, next) {
// Default to 500 if no statusCode was set (programmer error / unexpected)
const statusCode = err.statusCode ?? 500
const isOperational = statusCode < 500
// Log with full detail — server-side only:
if (isOperational) {
logger.warn({ err, reqId: req.id, path: req.path }, 'Operational error')
} else {
logger.error({ err, reqId: req.id, path: req.path, body: req.body }, 'Unhandled error')
}
// Send a safe, structured response to the client:
res.status(statusCode).json({
error: isOperational ? err.message : 'An unexpected error occurred',
code: isOperational ? err.code : 'INTERNAL_ERROR',
...(isOperational && err.details ? { details: err.details } : {}),
requestId: req.id, // lets the user quote it to support
})
}Registering the handler
app.js
import express from 'express'
import { router } from './routes/index.js'
import { errorHandler } from './middleware/errorHandler.js'
import { notFoundHandler } from './middleware/notFoundHandler.js'
const app = express()
app.use(express.json())
// ...other global middleware...
app.use('/api', router) // all application routes
// 404 for unknown routes — BEFORE the error handler:
app.use(notFoundHandler)
// Error handler — LAST:
app.use(errorHandler)middleware/notFoundHandler.js
import { NotFoundError } from '../errors/index.js'
export function notFoundHandler(req, res, next) {
next(new NotFoundError(`Route ${req.method} ${req.path} not found`))
}Throwing from routes — the full chain
import { asyncHandler } from '../utils/asyncHandler.js'
import { NotFoundError, ValidationError } from '../errors/index.js'
router.get('/users/:id', asyncHandler(async (req, res) => {
const user = await db.users.findById(req.params.id)
if (!user) throw new NotFoundError('User') // → errorHandler receives this
res.json(user)
}))
// Synchronous throw also works — Express catches it:
router.get('/sync', (req, res) => {
throw new ValidationError('Bad input') // → errorHandler receives this
})Validation errors with field details
// A ValidationError with per-field details:
throw new ValidationError('Invalid request body', [
{ field: 'email', message: 'Must be a valid email address' },
{ field: 'age', message: 'Must be a positive integer' },
])
// The handler sends this (statusCode = 400):
{
"error": "Invalid request body",
"code": "VALIDATION_ERROR",
"details": [
{ "field": "email", "message": "Must be a valid email address" },
{ "field": "age", "message": "Must be a positive integer" }
],
"requestId": "abc-123"
}Checklist
Exactly 4 args on the error handler function.
Registered last — after all routes and other middleware.
404 handler before the error handler — routes that don't match need a
next(new NotFoundError(...)).Never expose stack traces to the client.
Include a requestId so users can report issues that correlate with server logs.
Log operational and programmer errors differently — warn vs error severity.