Middleware Execution Order
Express does not have a clever scheduler or priority system for middleware. It runs your middleware and route handlers in the exact order you register them, walking a single linear stack from top to bottom. Because each function decides whether to pass control onward by calling next(), the registration order of app.use() and your routes is one of the most consequential design decisions in an Express app. Getting it wrong leads to subtle bugs: undefined request bodies, authentication that never runs, or error handlers that are silently skipped.
The middleware pipeline is sequential
When a request arrives, Express builds a matching stack of middleware and handlers, then executes them one at a time. A function runs, does its work, and calls next() to hand off to the next matching entry. If it never calls next() (and never sends a response), the request hangs forever.
const express = require("express");
const app = express();
app.use((req, res, next) => {
console.log("1: first middleware");
next();
});
app.use((req, res, next) => {
console.log("2: second middleware");
next();
});
app.get("/", (req, res) => {
console.log("3: route handler");
res.send("Hello");
});
app.listen(3000);
Output:
1: first middleware
2: second middleware
3: route handler
Reorder the app.use() calls and the log order changes accordingly. There is no magic — position in source determines position in the pipeline.
Express matches middleware by both registration order and path. A middleware registered without a path (
app.use(fn)) runs for every request; one registered with a path (app.use("/api", fn)) only runs when the request URL matches that prefix.
Pitfall: registering the body parser too late
express.json() populates req.body by reading and parsing the request stream. If a route that needs req.body is registered before the parser, the body will be undefined when that handler runs, because the parser middleware never executed first.
// WRONG — route is registered before the body parser
app.post("/users", (req, res) => {
// req.body is undefined here
res.json({ received: req.body });
});
app.use(express.json());
Output:
{ "received": undefined }
Move the parser above the routes so it runs first:
// CORRECT — parser runs before any route
app.use(express.json());
app.post("/users", (req, res) => {
res.json({ received: req.body });
});
Output:
{ "received": { "name": "Ada" } }
Pitfall: authentication after the routes
Authentication and authorization middleware must run before the handlers they protect. If you register an auth guard after a route, the route has already responded and the guard never gets a chance to reject the request.
// WRONG — the protected route runs before the guard
app.get("/admin", (req, res) => {
res.send("secret dashboard"); // served to everyone
});
app.use((req, res, next) => {
if (!req.headers.authorization) return res.status(401).send("Unauthorized");
next();
});
Register the guard first, or scope it to a path so it covers the routes that follow:
// CORRECT — guard runs before protected routes
function requireAuth(req, res, next) {
if (!req.headers.authorization) return res.status(401).send("Unauthorized");
next();
}
app.use("/admin", requireAuth);
app.get("/admin", (req, res) => {
res.send("secret dashboard");
});
How ordering rules combine
The table below summarizes how different registrations interact in a typical pipeline.
| Registration | When it runs | Typical position |
|---|---|---|
app.use(express.json()) | Every request, parses body | Near the top |
app.use(logger) | Every request | Very top |
app.use("/api", router) | Requests under /api | Middle |
app.get("/users", handler) | Matching method + path | After parsers |
404 fallback app.use(notFound) | Any unmatched request | After all routes |
app.use(errorHandler) | When next(err) is called | Very bottom |
Error-handling middleware (the four-argument form (err, req, res, next)) is special: Express only invokes it when something passes an error to next(), and it must be registered last so it sits below every route that might fail.
app.use(express.json());
app.get("/data", async (req, res, next) => {
try {
const data = await loadData();
res.json(data);
} catch (err) {
next(err); // forwards to the error handler below
}
});
// 404 handler: runs only if no route matched
app.use((req, res) => {
res.status(404).json({ error: "Not Found" });
});
// Error handler: must be registered last
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: "Internal Server Error" });
});
In Express 5.x, errors thrown (or rejected promises) inside an async route handler are automatically forwarded to your error-handling middleware, so you can drop many
try/catchblocks. In 4.x you must still callnext(err)yourself. Either way, the error handler must remain at the bottom of the stack.
Routers inherit the same rules
A Router is a mini-pipeline with its own ordered stack. Middleware mounted on a router runs in the order you add it, and the router itself runs at whatever point you mount it on the app.
const router = express.Router();
router.use((req, res, next) => {
req.tenant = req.headers["x-tenant"] ?? "default";
next();
});
router.get("/profile", (req, res) => {
res.json({ tenant: req.tenant });
});
app.use("/account", router); // router's stack runs only under /account
Best Practices
- Register global parsers and security middleware (
express.json(), CORS, helmet, logging) at the very top, before any route. - Put authentication and authorization guards before the routes they protect, or scope them with a path prefix.
- Define specific routes before broad catch-alls so the broad ones do not shadow the specific handlers.
- Place your 404 fallback after all routes and your error handler dead last (four-argument signature).
- Always call
next()(or send a response) in custom middleware — a forgottennext()silently hangs the request. - Keep router-level middleware close to the routes it serves to make the execution order obvious to future readers.