How middleware chains actually execute — next(), order, and error middleware
A middleware function only moves to the next one because it explicitly calls next() — nothing automatic advances the chain — and that one fact explains both why registration order genuinely matters and why a forgotten next() silently hangs a request forever.
3 min read
The chain only advances because each function explicitly says so
app.use((req, res, next) => {
console.log("first middleware");
next(); // explicitly hands control to the NEXT middleware in the chain
});
app.use((req, res, next) => {
console.log("second middleware");
next();
});
app.get("/", (req, res) => {
res.send("route handler — the LAST link in this chain");
});Every middleware function receives next as its third argument, and calling it is what moves execution to the next registered middleware (or, eventually, the matching route handler) — there's no automatic progression at all. This is the single fact that explains the entire middleware model: a request literally works its way down a chain of functions, one explicit next() call at a time, and any function in that chain can choose to not call next() — ending the chain there, usually by sending a response instead.
The classic bug: forgetting next() hangs the request forever
app.use((req, res, next) => {
console.log("logged the request"); // does its job...
// ...but FORGETS to call next() — the request just HANGS, no response ever sent
});
app.get("/", (req, res) => { res.send("never reached"); }); // this line never runs for ANY requestA middleware that does its work but never calls next() (and never sends a response itself) leaves the request permanently stuck — nothing times it out automatically, the client just waits, and eventually gets a timeout error from its own side, not from the server. This is a genuinely common, real bug specifically because it's easy to write a middleware that "just logs something" or "just checks something" and forget the next() call at the end, especially inside an if branch where the "happy path" calls next() but an early-return branch doesn't.
Registration order determines execution order — genuinely, not just conventionally
app.use(express.json()); // runs FIRST — body must be parsed before anything reads req.body
app.use(authenticate); // runs SECOND — needs to check auth before route logic runs
app.get("/profile", (req, res) => { /* runs THIRD — by now, req.body and req.user are both ready */ });
// Reversed order would BREAK this — authenticate() running before express.json()
// might need req.body (e.g., a token in the body) that hasn't been parsed yetExpress registers and runs middleware in exactly the order app.use()/route methods were called — this isn't a convention worth following loosely, it's the literal execution order, which means a middleware that depends on something an earlier middleware sets up (like req.body from express.json(), or req.user from an authentication middleware) genuinely breaks if registered before its dependency instead of after. Reading a stack of app.use() calls top-to-bottom is reading the actual, real sequence a request will pass through.
Error-handling middleware: the one exception, identified by its FOUR parameters
app.get("/risky", (req, res, next) => {
try {
doSomethingThatMightThrow();
} catch (err) {
next(err); // passing an argument to next() SKIPS all normal middleware, jumps straight to error handlers
}
});
// Registered LAST, after all other app.use()/routes — Express recognizes it by its FOUR parameters
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: "Something went wrong" });
});Calling next(err) with an argument (instead of calling it with nothing) tells Express this is an error, and Express skips every remaining normal middleware and route handler, jumping directly to the nearest error-handling middleware — a middleware function distinguished purely by having exactly four parameters (err, req, res, next) instead of three, which Express uses to recognize it as an error handler specifically. Error-handling middleware is conventionally registered last, after every other route and middleware, since it needs to be reachable from an error thrown anywhere earlier in the chain.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What actually advances execution from one middleware to the next in Express?
2. What happens when a middleware does its work but forgets to call next() or send a response?
3. How does Express distinguish error-handling middleware from normal middleware?