Error Middleware

Four Arguments — Express's Special Signature

Error Middleware

Error middleware has a 4-arg signature. Mount last. Centralize all error responses here.

4 min read Level 2/5 #express#middleware#errors
What you'll learn
  • Recognize the (err, req, res, next) signature
  • Trigger error middleware
  • Build a typed error class

Express recognizes a middleware as an error handler by its signature: it takes four arguments instead of three.

The Signature

app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({ error: "internal server error" });
});

That extra first parameter (err) is the trigger.

Trigger It

Three ways to send an error to this middleware:

1. Call next(err)

app.get("/users/:id", (req, res, next) => {
  if (!validId(req.params.id)) {
    return next(new Error("bad id"));
  }
  res.json(loadUser(req.params.id));
});

2. Throw (Express 5 only)

app.get("/users/:id", (req, res) => {
  if (!validId(req.params.id)) {
    throw new Error("bad id");   // Express 5 catches sync AND async throws
  }
  res.json(loadUser(req.params.id));
});

In Express 4, async throws don’t get caught — see the next lesson.

3. Async Rejection (Express 5)

app.get("/users/:id", async (req, res) => {
  const user = await db.users.findById(req.params.id);
  // if findById rejects, Express 5 forwards to the error middleware
  res.json(user);
});

A Typed Error Class

export class HttpError extends Error {
  constructor(status, code, message) {
    super(message);
    this.status = status;
    this.code = code;
  }
}

Now your error middleware can branch on type:

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, generic response
  console.error("unexpected error", err);
  res.status(500).json({
    error: { code: "internal", message: "internal server error" },
  });
});

Order

Mount error middleware after every route:

// routes
app.use("/api", apiRouter);

// 404
app.use((req, res) => {
  res.status(404).json({ error: "not found" });
});

// errors (must be last)
app.use(errorHandler);

If you mount the error handler too early, requests skip past it because they haven’t errored yet.

Don’t Leak Stacks to Clients

Send a generic message. Log the stack server-side:

app.use((err, req, res, next) => {
  console.error(err);   // full stack in your logs

  res.status(500).json({
    error: { message: "internal server error" },
  });
});

In development, you might include err.stack in the response for convenience. Never in production.

Async Error Handling →