Error Handling

Centralize It — and Don't Leak Internals

Error Handling

One error-handling middleware. Custom error classes. Never leak stack traces to clients.

4 min read Level 2/5 #nodejs#express#errors
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

TypeExampleResponse
OperationalDB down, request timeout, validation failHandle, log, return user-friendly error
Programmerundefined.foo, typo, logic bugLog 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 →