Express.js Interview Questions: Middleware Order, Async Errors, and Response Lifecycles

Practice Express.js middleware order, Express 5 async error forwarding, headers-sent recovery, and response-lifecycle interview traces with examples.

Author: PracHub

Published: 9/28/2026

Express.js Interview Questions: Middleware Order, Async Errors, and Response Lifecycles

September 28, 2026

Quick Overview

Ordered middleware trace, next versus next(err), Express 5 promise boundaries, partial streaming responses, and HTTP regression cases.

Software EngineerFree

An Express route can look correct and still hang forever, send two responses, or turn a recoverable error into a broken connection. In an interview, the useful skill is to trace the request through registration order, the next() decisions, asynchronous work, and the point at which response headers have already gone out. That trace explains more bugs than memorizing a list of middleware types.

Official fact: Express's middleware guide describes the ordered stack and the need to end a response or call next. The error-handling guide says Express 5 forwards rejected promises returned by handlers or middleware and requires special treatment after headers are sent. Inference: a strong answer identifies the last stage reached and the first missing transition. The examples below are editorial exercises, not claims about a particular interviewer's question set. Version matters: any automatic async forwarding claim here is about Express 5, not all Express 4 applications.

Editorial illustration of an ordered Express middleware pipeline and error detour

Five verified PracHub questions to practice

Question 1: What happens when middleware runs in the wrong order?

Express evaluates matching middleware and routes in registration order. A middleware function can modify req or res, end the response, call next() to continue, or pass an error through next(err). If it neither ends nor advances, the request hangs. If it calls next after sending a response, downstream code may attempt a second send. If a parser is registered after a route that reads req.body, the route can see missing parsed data. If authentication is registered after a route, the route may bypass it.

A useful answer begins with a concrete stack:

app.use(express.json());
app.use(authenticate);
app.post('/orders', validateOrder, createOrder);
app.use(notFound);
app.use(errorHandler);

This shape says the JSON parser and authentication are considered before the order route, a matching route can run its validation and creation handlers, and error handling is registered after the routes. It does not prove every path is secure: mount paths, route-specific middleware, and any earlier response-ending function still matter. It also does not mean notFound will run after a handler that already sent a response and stopped the chain.

Trace a POST with invalid JSON. The parser may pass an error to error middleware before authenticate or createOrder can run. Trace a valid POST with no token: authenticate should end with an unauthorized response or call next(err), according to its contract. Trace a valid authenticated POST: each function must either send once or advance. This is more precise than saying “middleware runs before routes,” because routes themselves are ordered middleware layers. Design a trie-based URL router with wildcards is a useful adjacent practice question for thinking about route matching separately from middleware order.

Question 2: What is the difference between next(), next(err), and a response?

Calling next() advances to the next eligible non-error handler in the stack. Calling next(err) enters error-handling flow and skips remaining ordinary handlers. Sending a response with res.json, res.send, or res.end completes the handler's response work; you generally should not advance into another response-writing handler afterward. These are control-flow decisions, not stylistic preferences.

Imagine validateOrder detects a missing item. A clear implementation can send a 400 response and return, or create a typed client error and call next(err) for centralized formatting. Mixing both is a defect. The interview answer should mention the return after a send, because code below the send can still execute within the same JavaScript function. “Express stops the function for me” is false. Likewise, a middleware that calls next() and then continues to mutate shared state may race conceptually with downstream work or make the outcome hard to reason about.

Error handlers have four parameters: err, req, res, next. Placement matters. A custom error handler placed before the routes it is supposed to catch can be bypassed by later errors. But do not simply say “always last” without noticing multiple routers and scoped error handlers; the relevant rule is that an error must have a later matching error handler or fall to Express's default handler. The official error guide gives the canonical shape.

Question 3: How do async failures reach the error handler?

For Express 5, an async route that throws or returns a rejected promise is forwarded into error handling automatically. That is documented in the Express 5 migration guide. In Express 4, applications commonly use explicit try/catch with next(err) or a wrapper; do not silently apply Express 5 behavior to an older codebase. Even in Express 5, the returned-promise boundary matters. A promise started inside a handler and neither returned nor awaited may reject outside Express's observed chain. A callback API also needs its error passed to next if it fails asynchronously.

A safe Express 5 example is:

app.get('/orders/:id', async (req, res) => {
  const order = await loadOrder(req.params.id);
  if (!order) return res.status(404).json({ error: 'Not found' });
  return res.json(order);
});

If loadOrder rejects, Express 5 routes the rejection to error handling. The early return after the 404 prevents a later second response. Now change the example: fire loadOrder().then(...) without returning the promise, then let the handler exit. Express cannot infer an unreturned asynchronous chain; you must return it or explicitly route the rejection. A setTimeout callback that throws is another separate boundary. The correct answer does not claim “async/await fixes errors”; it names what Express can observe.

An interviewer may ask whether to add catch(next) to every Express 5 async handler. It is usually redundant when the handler returns its promise, and careless wrappers can call next twice. The better rule is to use one error-routing mechanism per path and test one rejected dependency. Debug and Harden a Ticketing Backend API is a relevant exercise for tracing failure propagation from a route to its client-visible result.

Question 4: What changes after headers have been sent?

Before response headers are sent, an error handler can still choose an HTTP status and body. After headers are sent, it cannot replace the status or start a new JSON error response. Express's error-handling documentation instructs a custom handler to delegate with next(err) when res.headersSent is true; the default handler may close the connection when an error arrives during a streamed response. This is why stream failures need a different recovery story from ordinary validation failures.

Consider a route that begins streaming a CSV file, writes several rows, then its database cursor fails. Sending res.status(500).json(...) now would try to create a new response after the client received part of a CSV. The handler should log the failure with a request identifier, stop the stream, and delegate or destroy the connection according to the stream contract. The client sees an incomplete response and must decide whether a retry is safe. A half-delivered body is not a clean 500. If a job can be validated before streaming, move that validation earlier, but do not assume every I/O failure can be predicted.

Also distinguish headersSent from writableEnded. Headers may be sent while the body is still streaming; a completed response has a different lifecycle. A listener for the response's finish event can record that bytes were handed to the underlying system; a close event can indicate an early termination. Such instrumentation helps diagnose a missing response or abort, but you should avoid claiming it proves the client consumed the entire body. The key interview boundary is whether the server can still change the response contract.

Technical decision diagram for rejected promises before and after the response-header boundary

A worked trace: the client hangs only when validation fails

Suppose an order API has express.json, auth, validateOrder, and createOrder. Successful orders return 201. An invalid order causes the client to wait until its timeout. Start by logging entry and exit for each middleware with a request ID. If validateOrder detects a problem and logs it, but neither sends a response nor calls next(err), the request has no terminal transition. A missing return after a validation branch could instead fall through to next() and create an invalid order. These two bugs look different at the client, so the trace matters.

Now add an asynchronous inventory lookup inside validateOrder. If it rejects and the app runs Express 5 with an awaited promise, the error should reach error middleware. If the code starts the lookup in an unreturned callback, the request may still hang or fail outside Express's normal error path. The repair is to return or await the work and give every branch exactly one action: respond, advance, or error. A regression test should cover valid input, invalid input, rejected lookup, and a slow lookup with a defined timeout policy. Inspect both HTTP result and absence of duplicate write.

A response-state matrix clarifies the action:

Observed stateNext safe stepUnsafe assumption
No response, validation failedSend one 4xx or call next(err)That logging ends a request
No response, dependency rejectedForward once to error handlerThat all floating promises are observed
Headers already sent, stream failedDelegate/terminate and record partial resultThat a fresh JSON 500 can replace bytes
Response finishedStop writing and log completionThat next() must always be called

Bootstrap Service: Aggregate Downstream Services and Return the Right HTTP Status lets you rehearse another source of partial results: a downstream service may fail before you commit a response, and your API must choose a deliberate status. Production-Ready Event Ingestion Handler: Validation, Idempotent Writes, DLQ, Metrics extends the reasoning from HTTP control flow to durable side effects.

How do you test the response lifecycle?

Use an HTTP-level test rather than calling a route function as though it were a pure function. Register the real middleware stack in a test app, substitute a deterministic dependency, and assert status, body, and side effects. A rejected promise test is essential for Express 5 behavior. A parser-error test proves the error handler is reachable from middleware before the route. A streaming test can use a controlled stream that emits an error after its first chunk; assert that the server does not try to send a second JSON document.

Keep assertions tied to the contract. A test that only checks errorHandler was called can pass while the request hangs. A test that only checks status can miss a double database write. For an endpoint that mutates state, test whether a retry can repeat the mutation, and identify the persistence boundary. Explain HTTP methods and your usage helps connect route choice to request semantics, but the actual safety of retries depends on implementation, not method names alone.

In your interview response, draw the stack from top to bottom, mark every next or send, and place a vertical line where headers go out. State the Express major version before discussing promise rejections. Then identify one test that would expose the suspected bug. That is a complete causal answer: order, error propagation, and lifecycle all become visible.

Sources and further reading


Comments (0)