Implementing Error Handling
1. Understanding Error Flow
Sync throw ──┐
Async w/await ──┤ Express 5
next(err) ──┼──▶ Error middleware (4-arg)
Promise reject ──┘ └─ res.status(...).json({...})
Express 4 caveat: async errors NOT auto-caught — wrap with try/catch or asyncHandler
| Express 4 | Express 5 |
|---|---|
| Async rejections crash unless wrapped | Auto-forwarded to error middleware EXPRESS 5 |
2. Creating Error Middleware
Example: Standard error handler
app.use((err, req, res, next) => {
if (res.headersSent) return next(err); // delegate to default
const status = err.status || err.statusCode || 500;
logger.error({ err, id: req.id, status }, err.message);
res.status(status).json({
error: { message: err.expose ? err.message : "Internal Server Error", code: err.code }
});
});
3. Handling Synchronous Errors
Example: Sync throw auto-caught
app.get("/users/:id", (req, res) => {
const id = Number(req.params.id);
if (!Number.isInteger(id)) throw new HttpError(400, "Invalid id");
res.json({ id });
});
4. Handling Async Errors
Example: Express 5 native
// Express 5 — async errors auto-forwarded
app.get("/users/:id", async (req, res) => {
const user = await db.users.findById(req.params.id);
if (!user) throw new HttpError(404);
res.json(user);
});
Example: Express 4 — wrap with asyncHandler
const asyncHandler = (fn) => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
app.get("/users/:id", asyncHandler(async (req, res) => { /* ... */ }));
5. Passing Errors to next()
| Pattern | Effect |
|---|---|
next(err) | Skip to error middleware |
next() (no arg) | Continue normally |
throw err in sync | Auto-caught by Express |
throw in async (Express 5) | Auto-forwarded |
6. Creating Custom Error Classes
Example: HttpError hierarchy
export class HttpError extends Error {
constructor(status, message = "", code = null) {
super(message);
this.name = this.constructor.name;
this.status = status;
this.code = code;
this.expose = status < 500; // safe to send to client
Error.captureStackTrace?.(this, this.constructor);
}
}
export class NotFoundError extends HttpError { constructor(m="Not found") { super(404, m); } }
export class ValidationError extends HttpError { constructor(m="Bad request", details) { super(400, m); this.details = details; } }
export class UnauthorizedError extends HttpError { constructor(m="Unauthorized") { super(401, m); } }
7. Setting Error Status Codes
| Property | Convention |
|---|---|
err.status | Express convention |
err.statusCode | http-errors / Boom |
err.code | String code (e.g., USER_NOT_FOUND) |
err.expose | Send message to client? |
8. Formatting Error Responses
Example: Problem Details (RFC 7807)
app.use((err, req, res, next) => {
const status = err.status || 500;
res.status(status).type("application/problem+json").json({
type: err.type || `https://errors.example.com/${err.code || "internal"}`,
title: err.expose ? err.message : "Internal Server Error",
status,
detail: err.detail,
instance: req.originalUrl,
requestId: req.id
});
});
9. Logging Errors
| Library | Notes |
|---|---|
pino | Fast structured JSON logger RECOMMENDED |
winston | Multi-transport, mature |
bunyan | JSON logger (older) |
Example: pino in error handler
logger.error({
err: { message: err.message, stack: err.stack, code: err.code },
req: { id: req.id, method: req.method, url: req.originalUrl, userId: req.user?.id }
}, "Request failed");
10. Handling 404 Not Found
Example: 404 + delegate
// Place AFTER all routes, BEFORE error handler
app.use((req, res, next) => next(new HttpError(404, `Not found: ${req.method} ${req.originalUrl}`)));
11. Differentiating Development vs Production Errors
Example: Hide stack in prod
app.use((err, req, res, next) => {
const status = err.status || 500;
const body = { error: err.expose ? err.message : "Internal Server Error" };
if (process.env.NODE_ENV !== "production") {
body.stack = err.stack;
body.detail = err;
}
res.status(status).json(body);
});
| Environment | Behavior |
|---|---|
| development | Full stack trace in response |
| production | Generic message, log full details |
12. Using express-async-errors Package
Example: Express 4 patch
// app.js — first line
import "express-async-errors";
import express from "express";
// Now async route handlers forward rejections automatically
Note: Not needed in Express 5 — async error forwarding is built-in.