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 4Express 5
Async rejections crash unless wrappedAuto-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()

PatternEffect
next(err)Skip to error middleware
next() (no arg)Continue normally
throw err in syncAuto-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

PropertyConvention
err.statusExpress convention
err.statusCodehttp-errors / Boom
err.codeString code (e.g., USER_NOT_FOUND)
err.exposeSend 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

LibraryNotes
pinoFast structured JSON logger RECOMMENDED
winstonMulti-transport, mature
bunyanJSON 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);
});
EnvironmentBehavior
developmentFull stack trace in response
productionGeneric 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.