Walk me through the middleware chain of an Express service. What has to come before what, and why?
Middleware runs in registration order, so ordering is behaviour rather than style: the request id precedes logging, the body parser precedes anything reading the body, authentication precedes authorisation, and the error handler goes last because Express only searches forward from the failure.
What the interviewer is scoring
- Whether the candidate justifies each ordering constraint by what the middleware needs rather than by convention
- That the error handler's four-argument signature and terminal placement are both known
- Does the candidate handle the rejected promise in an async handler
- Whether body parsing is scoped or limited rather than applied globally without thought
- That a response-time or logging middleware is understood to run its tail after the handler
Answer
Order is behaviour
Express holds a single ordered list of middleware and walks it per request, calling each until one ends the response or the list is exhausted. There is no dependency resolution and no priority. The order you register is the order that runs, so ordering is a functional decision that happens to look like a stylistic one.
A defensible chain, and what each position is buying:
Request id first, because everything after it wants to reference the id. Generate one or adopt an incoming correlation header, and put it somewhere the rest of the request can reach.
Logging second, so it can record the id and so its timing brackets everything below it. A logger registered after authentication cannot log a request rejected by authentication, which is exactly the request you most want in the log.
Security headers, CORS, rate limiting before the expensive work. Rate limiting after body parsing means you parse a megabyte of JSON for a request you were about to refuse, which turns your rate limiter into an amplifier.
Body parsing before anything that reads the body, and — importantly — after the routes that do not need it.
Authentication, establishing who the caller is, then authorisation, deciding what they may do. That order is forced: you cannot check permissions for an identity you have not established. They are separate middleware because authentication is global and authorisation is per route.
Routes, then the 404 handler for anything unmatched, then the error handler last.
Why the error handler must be last
Express distinguishes error middleware by arity: four parameters means error handler.
// Four parameters. Omit `next` and this becomes ordinary middleware
// that never runs on an error.
app.use((err, req, res, next) => {
req.log.error({ err }, "request failed");
res.status(err.status ?? 500).json({ error: err.publicMessage ?? "Internal error" });
});
The signature is a genuine trap, because a handler written with three parameters is silently registered as normal middleware and never fires. There is no warning; errors simply fall through to Express's default handler, which in production returns a bare 500.
Placement matters for a reason people often get backwards. When next(err) is called, Express searches forward from the current position for the next error handler. Anything registered earlier is not considered. So an error handler registered before the routes will never see an error from a route, and the fix is not clever configuration — it is moving it to the bottom.
Async handlers and the rejection that vanishes
Express 4 predates promises, so it invokes handlers and ignores what they return. A rejected promise from an async handler is therefore not routed anywhere.
// Express 4: the rejection is unhandled. The client waits until it times out
// and the error handler never runs.
app.get("/orders/:id", async (req, res) => {
const order = await repo.find(req.params.id); // throws
res.json(order);
});
The classic fix is a wrapper that catches and forwards:
const wrap = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
app.get("/orders/:id", wrap(async (req, res) => { /* ... */ }));
Express 5 changed this: a returned promise that rejects is now forwarded to the error handler automatically, which removes the need for the wrapper on route handlers. It is worth being explicit about which major version you are on, because a team that upgrades and keeps the wrapper is harmless while a team that assumes the behaviour on version 4 has a hanging request per failure.
Two details on the body parser
Applying express.json() globally is the default and is worth reconsidering. A webhook that verifies an HMAC signature needs the raw bytes, and a parser that has already consumed and re-encoded the stream has destroyed the ability to verify, since re-serialising JSON does not reproduce the original bytes. Those routes need the raw body captured before or instead of parsing.
Always set a size limit. Without one, the parser will buffer whatever is sent, and an unauthenticated endpoint accepting arbitrarily large JSON is a trivially exploitable memory exhaustion. Express's default limit is 100 kB, which is a sensible baseline, and the point is to choose it deliberately per route rather than inherit it.
Middleware runs on the way back too
A subtlety that catches people writing timing or logging middleware. Code before next() runs on the way in; to run something after the handler you attach to the response's finish event.
app.use((req, res, next) => {
const start = process.hrtime.bigint();
// 'finish' fires once the response has been flushed to the socket.
res.on("finish", () => {
const ms = Number(process.hrtime.bigint() - start) / 1e6;
req.log.info({ status: res.statusCode, ms }, "request complete");
});
next();
});
Note what this cannot do: set a response header with the duration. Headers are sent with the first byte of the body, so by the time finish fires they are long gone. Setting a header after the response has started throws ERR_HTTP_HEADERS_SENT, which is one of the more confusing errors to receive because the offending code looks correct in isolation. If you need a timing header, it has to be written before the body — res.writeHead time, not finish time.
Registration order is not a convention, it is the execution order. The error handler goes last because Express only looks forward, and a three-argument error handler is not an error handler at all.
Likely follow-ups
- You register the error handler before your routes. What happens?
- An async route handler throws. Does your error middleware see it?
- Why should the body parser have a size limit, and what happens without one?
- Where does a response-time header have to be set, given that headers flush with the first write?
Related questions
- A request fails but your Express error-handling middleware never runs. How do you work out why?mediumAlso on express and error-handling3 min
- Your function returns nil on the success path, the caller checks err != nil, and the check fires anyway. What happened?hardAlso on error-handling4 min
- You added an authorisation policy to an ASP.NET Core endpoint and it is not being enforced. Where do you look?mediumAlso on middleware3 min
- How does your order placement change on a venue that allocates pro rata within a price level instead of first in, first out?hardSame kind of round: concept6 min
- Adapter, decorator and facade - what concrete problem does each one solve, and where would reaching for it be over-engineering?mediumSame kind of round: concept6 min
- How would you design a thread-safe component, and why is adding synchronized to every method not a design?hardSame kind of round: concept7 min
- Where does encapsulation or polymorphism change a design, and how do you decide between composition and inheritance?mediumSame kind of round: concept6 min
- When is a factory the right answer, when is a builder, and when should you just call the constructor?mediumSame kind of round: concept7 min