A Production-Grade Error Handler
One error handler to rule them all: typed errors, safe client messages, correlated logs, and no stack traces leaking to users.
A centralized error handler is the single place that turns any thrown value into an HTTP response. Get it right once and every route inherits consistent behavior. The recipe has four responsibilities.
1. Distinguish operational from programmer errors
An operational error is expected (not found, validation failed, unauthorized) — you designed for it and it carries a safe status and message. A programmer error is a bug (undefined is not a function). Never leak the second kind to clients; log it fully, return a generic 500.
2. Normalize the status and body
const status = err.status ?? 500;
const body = { error: status < 500 ? err.message : 'Internal Server Error' };3. Log with correlation
Attach the request id and the full stack to your logger — never to the response body in production.
4. Respect NODE_ENV
In development it is fine to echo the stack to speed debugging; in production, hide it. Keep the four-argument signature or Express treats the function as ordinary middleware.
Example
// errors.js
export class AppError extends Error {
constructor(message, status = 500, { expose = status < 500 } = {}) {
super(message);
this.status = status;
this.expose = expose; // may the message be shown to the client?
this.isOperational = true; // we anticipated this one
}
}
// middleware/errorHandler.js
export function errorHandler(logger) {
const isProd = process.env.NODE_ENV === 'production';
// Keep the four-arg signature — this is load-bearing.
return (err, req, res, next) => {
if (res.headersSent) return next(err); // let Express abort the stream
const status = Number.isInteger(err.status) ? err.status : 500;
const expose = err.expose ?? status < 500;
logger.error({
requestId: req.id,
status,
msg: err.message,
stack: err.stack,
operational: Boolean(err.isOperational),
});
res.status(status).json({
error: expose ? err.message : 'Internal Server Error',
requestId: req.id,
...(isProd ? {} : { stack: err.stack }),
});
};
}
// app.js (last middleware)
// app.use(errorHandler(logger));When to use it
- A production API's error handler sends structured JSON to the client but also ships error details to a Sentry DSN for monitoring.
- A developer creates separate error handler modules for operational errors (e.g., 404, validation) and programmer errors (e.g., 500), applying different logging levels.
- An error handler checks err.isJoi to detect Joi validation errors and automatically maps them to a 422 status with detailed field messages.
More examples
Differentiating error types
Distinguishes operational errors (safe to expose) from unexpected programmer errors (hide message, log internally).
class AppError extends Error {
constructor(message, status) { super(message); this.status = status; this.isOperational = true; }
}
app.use((err, req, res, next) => {
if (err.isOperational) {
return res.status(err.status).json({ error: err.message });
}
// Unexpected programmer error — log and hide details
console.error('UNHANDLED:', err);
res.status(500).json({ error: 'Internal Server Error' });
});Error handler with Sentry reporting
Registers Sentry's error handler middleware before the custom handler so all 500-level errors are captured and reported.
const Sentry = require('@sentry/node');
app.use(Sentry.Handlers.errorHandler()); // must be before custom error handler
app.use((err, req, res, next) => {
const status = err.status || 500;
res.status(status).json({
error: status < 500 ? err.message : 'Internal Server Error',
requestId: req.id,
});
});Validation-aware error handler
Handles Mongoose ValidationError, JWT errors, and duplicate-key errors with appropriate status codes before falling back to a generic 500.
app.use((err, req, res, next) => {
if (err.name === 'ValidationError') { // Mongoose
return res.status(422).json({ errors: Object.values(err.errors).map(e => e.message) });
}
if (err.name === 'JsonWebTokenError') { // jsonwebtoken
return res.status(401).json({ error: 'Invalid token' });
}
if (err.code === 11000) { // MongoDB duplicate key
return res.status(409).json({ error: 'Duplicate entry' });
}
res.status(err.status || 500).json({ error: err.message || 'Server error' });
});
Discussion