Centralize It — and Don't Leak Internals
Error Handling
One error-handling middleware. Custom error classes. Never leak stack traces to clients.
What you'll learn
- Author a central error middleware
- Throw typed errors from handlers
- Distinguish operational vs programmer errors
Error handling spread across every handler is brittle. One centralized error middleware is the standard pattern.
Custom Error Class
export class HttpError extends Error {
constructor(status, code, message) {
super(message);
this.status = status;
this.code = code;
}
}
export const notFound = (msg = "not found") => new HttpError(404, "not_found", msg);
export const badRequest = (msg) => new HttpError(400, "bad_request", msg); Throw From Handlers
app.get("/users/:id", async (req, res) => {
const user = await db.findUser(req.params.id);
if (!user) throw notFound("user does not exist");
res.json(user);
}); In Express 5, thrown errors (sync or async) auto-route to the error middleware.
The Error Middleware
// MUST be last (after all routes)
app.use((err, req, res, next) => {
if (err instanceof HttpError) {
return res.status(err.status).json({
error: { code: err.code, message: err.message },
});
}
// Unexpected — log details, send generic response
console.error("unexpected error", err);
res.status(500).json({
error: { code: "internal", message: "internal server error" },
});
}); The pattern: known errors → exact response. Unknown errors → log deeply, respond generically.
Don’t Leak Stack Traces
Never send err.stack to clients. It can reveal file paths, code
structure, dep versions — gifts to attackers. Log them server-side
only.
404 Catch-All
Place between routes and the error middleware:
app.use((req, res) => {
res.status(404).json({ error: { code: "not_found" } });
});
app.use(errorMiddleware); Operational vs Programmer Errors
| Type | Example | Response |
|---|---|---|
| Operational | DB down, request timeout, validation fail | Handle, log, return user-friendly error |
| Programmer | undefined.foo, typo, logic bug | Log loudly, crash → restart |
Don’t try to “recover” from programmer errors — let the supervisor restart you. The state may be corrupt.
Don’t Forget unhandledRejection
process.on("unhandledRejection", (reason) => {
console.error("UNHANDLED REJECTION", reason);
process.exit(1);
}); Promises that reject with nothing catching them — these are usually bugs. Crash and let the supervisor restart.
Validation →