InteractiveFrameworks

Errors

Let routes throw and answer every error in one place: an error class that carries its status, a 404 for anything no route matched, body-parser's error for broken JSON, Express 5 catching async handlers, and a 500 that tells the client nothing about your internals.

What you'll learn

  • Write error-handling middleware with four parameters, mount it last, and answer every error in one shape
  • Throw from synchronous and async handlers alike, relying on Express 5 to route both to the error handler
  • Answer unmatched requests with a 404 middleware, and keep internal details out of 500 responses

Every route so far has answered its own errors: a 404 here, a 400 there, each written out where it happens. That spreads the error format across the whole app, and it covers only the errors someone thought of. A database that goes away, a typo that throws, a body that is not JSON: those reach Express with no route ready for them, and Express answers with a page of its own. This lesson moves error handling to one place, so that routes can simply throw and every error leaves the app in the same shape.

What Express does with an error

Anything that throws inside a handler, or is passed to next(err), skips every remaining ordinary middleware and route and goes to error-handling middleware. If there is none, Express's built-in handler answers. Running in production mode, as it does here, that handler sends a small HTML page saying Internal Server Error and logs the stack to the console; a thrown error with a status property of 4xx gets that status and its standard phrase instead.

Express 5 catches async errors. A rejected promise from an async handler goes to the error handling exactly like a synchronous throw. In Express 4 it did not: the rejection was unhandled, the request hung, and every async route needed a try/catch or a wrapper. In Express 5, throw inside async (req, res) => { … } just works.

Error-handling middleware

It is middleware with four parameters:

app.use((err: unknown, req: Request, res: Response, next: NextFunction) => {
  res.status(500).json({ error: 'Something went wrong' });
});

Express tells it apart from ordinary middleware by counting its parameters, so all four must be declared, even an unused next. It belongs last, after every route, because Express only reaches it by skipping forward. If a response has already started when an error arrives (res.headersSent), the handler cannot change its status any more; passing the error on with next(err) lets Express close the connection.

Errors that carry their status

A route that finds nothing wants to say "404" without knowing how the app formats errors. An error class that carries the status does it:

class HttpError extends Error {
  constructor(readonly status: number, message: string) {
    super(message);
  }
}

app.get('/owners/:id', (req, res) => {
  const owner = findOwner(req.params.id);
  if (!owner) throw new HttpError(404, 'No such owner');
  res.json(owner);
});

The error handler checks err instanceof HttpError and answers err.status with err.message. NestJS's HttpException is this same idea, built in.

Errors from middleware

Middleware fails too, and passes its errors on the same way. express.json() receiving broken JSON does not throw into your route; the route never runs. It calls next(err) with an error whose status is 400 and whose type is 'entity.parse.failed', which the error handler can recognise and answer in the API's own shape.

404: the request nothing matched

A request that no route answers is not an error to Express; it simply reaches the end of the chain, and Express answers Cannot GET /nope in HTML. Ordinary middleware mounted after all the routes catches those requests instead, since nothing before it answered them. A path that exists with a method no route declares ends up there too.

What a 500 must not say

An unexpected error's message is written for developers: a database address, a file path, a query. Sending it to the client hands that to anyone who can trigger the error. The client gets a generic message; the details go to the log, where console.error puts them.

Your task

  1. Write HttpError: an Error that also carries the status to answer with.
  2. GET /cats/:id throws HttpError(404, "Cat <id> not found") for a missing cat.
  3. POST /cats throws HttpError(422, "name must be a non-empty string") unless the name is a string with something other than spaces.
  4. After the routes, answer anything unmatched with 404 and { "error": "No route for <METHOD> <path>" }.
  5. Last, an error handler: an HttpError answers its status and { "error": <message> }; a body that is not valid JSON answers 400 and { "error": "The body is not valid JSON" }; anything else is logged and answers 500 and { "error": "Internal server error" }.

When it fails

  • Errors still come back as HTML: the error handler has three parameters, so Express treats it as ordinary middleware.
  • The error handler never runs: it is mounted before the routes.
  • "The database fails" contains database unavailable: the 500 sent err.message.
  • /nope answers Cannot GET /nope: the 404 middleware is missing, or is before the routes and answers everything.

Remember

  • Anything thrown, or passed to next(err), goes to error-handling middleware; Express 5 includes async handlers.
  • Error middleware has four parameters and goes last; a 404 middleware goes after the routes.
  • An error class with a status lets routes throw instead of answering.
  • A 500 says nothing specific to the client; the log gets the details.
Stuck? Show a hint

class HttpError extends Error with a status. Error middleware is (err, req, res, next) => { … }: all four parameters, or Express treats it as ordinary middleware; mount it after everything else. The 404 handler is ordinary middleware after the routes. express.json() reports a body it could not parse as an error whose type is 'entity.parse.failed'. Log an unexpected error with console.error, and answer only a generic message.