What this usually means
Express middleware executes in the order they are registered with app.use() or app.METHOD(). When you see out-of-order or skipped execution, the root cause is almost always: (1) route paths are too broad or too narrow, causing Express to match a later route first; (2) a middleware calls next(err) or throws synchronously, skipping non-error handlers; (3) a middleware calls next() asynchronously without returning or without catching rejections, leaving the request hanging; or (4) the middleware is defined after the route that should use it. These are not 'Express bugs'—they are design misunderstandings. The framework is ruthlessly linear: order of registration === order of execution, unless you short-circuit with an error or a mismatched path.
The first ten minutes — establish facts before touching code.
- 1List all middleware and route registrations in the exact order they appear in your main app file (or the file where you call app.use). Use `grep -n 'app\.use\|app\.get\|app\.post' app.js` to see the registration order.
- 2Check if any middleware calls next() inside an asynchronous callback without a return statement. Example: `someAsync().then(() => next())` – if `someAsync` rejects, next is never called.
- 3Verify that error-handling middleware (4 arguments) is registered AFTER all routes. If it's before, it will never catch errors.
- 4Add a simple logging middleware at the very top of your stack: `app.use((req, res, next) => { console.log('Request path:', req.path); next(); })`. Run a request – if the log appears after your route handler output, the middleware is registered after the route.
- 5Check for multiple routers mounted with `app.use('/path', router)`. The path prefix can cause sub-routers to handle requests earlier than expected if the parent path overlaps.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchapp.js or index.js – the file where you call `app.use()` and `app.get()` etc. This is ground zero for order issues.
- searchAny router files imported with `const router = express.Router()` – check the order of router.use() and router.get() inside each.
- searchMiddleware modules (e.g., authMiddleware.js) – look for next() calls inside async functions or Promises.
- searchError-handling middleware definition – confirm it has 4 parameters (err, req, res, next).
- searchPackage.json scripts – sometimes the entry point is different (e.g., bin/www) and the order differs from what you think.
- searchTest files – if you have integration tests, they may reveal the order of execution via logs.
Practical causes, not theory. These are the things you will actually find.
- warningMiddleware registered after the route it should protect – Express matches routes in order, so the first matching route wins.
- warningMissing next() call in a middleware – the request hangs and never reaches subsequent handlers.
- warningnext() called multiple times – Express throws an error (ERR_HTTP_HEADERS_SENT) but silently? Actually it sends a warning; but the order may seem corrupted downstream.
- warningAsynchronous middleware that does not catch rejections – an unhandled promise rejection ends the request silently, skipping all later handlers.
- warningOverlapping route paths – e.g., app.use('/users', router) and app.use('/users/:id', otherRouter) can cause unexpected matching.
- warningError thrown synchronously in middleware without a try-catch – Express catches it and passes to error middleware, skipping non-error middlewares.
Concrete fix directions. Pick the one that matches your root cause.
- buildReorder middleware registrations: place all global middlewares (auth, logging, etc.) BEFORE any route definitions. Routes should be at the bottom of the stack.
- buildEnsure every middleware calls next() exactly once. Use `return next()` inside callbacks to prevent accidental multiple calls.
- buildFor async middleware, wrap the entire body in a try-catch and call `next(err)` on rejection. Or use a wrapper like `asyncHandler = fn => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next)`.
- buildIf you need middleware to run only on specific routes, mount it directly on that route: `app.get('/protected', authMiddleware, handler)`. This avoids order surprises.
- buildFor error-handling middleware, define it last with exactly 4 arguments. It will catch any errors passed via `next(err)`.
- buildUse `app.use('/path', middleware)` only when you want middleware to run for all methods under that path. For specific HTTP methods, use `app.get('/path', middleware)`.
A fix you cannot prove is a guess. Close the loop.
- verifiedAdd a unique console log at the start of each middleware and route handler. Run a request that hits the route; the order of logs shows execution order.
- verifiedUse the `express-request-id` middleware to add a request ID, then log it in each middleware to trace a single request.
- verifiedWrite an integration test that sends a request and asserts the response headers or body, but also check that a specific side effect (like a database call) happened after auth middleware by inspecting logs.
- verifiedTemporarily remove all routes except one and add a simple middleware that sets a header; check the response headers to confirm the middleware executed.
- verifiedFor async middleware, intentionally make it reject and verify that the error-handling middleware catches it.
Things that make this bug worse or harder to find.
- warningDefining routes and middleware in separate files without a clear order – you lose sight of the registration sequence.
- warningPlacing error-handling middleware in the middle of the stack – it will only catch errors from middlewares before it.
- warningNot returning the result of next() inside a callback – e.g., `someAsync().then(() => { next(); })` should be `return someAsync().then(() => next())` or you might call next twice.
- warningAssuming Express middleware runs in parallel – it's strictly sequential per request.
- warningUsing `app.use()` without a path argument for global middleware, but then defining routes with `app.get('/')` – the order still matters based on registration.
Auth middleware skipped on /admin routes
Timeline
- 09:00Deploy new feature: admin dashboard with analytics endpoint.
- 09:15User reports that /admin/analytics returns data without authentication.
- 09:20Check code: auth middleware defined in auth.js, imported into app.js.
- 09:25Look at app.js: app.use('/admin', adminRouter) appears BEFORE app.use(authMiddleware).
- 09:30Also notice adminRouter has its own middleware for rate limiting, but no auth.
- 09:35Move app.use(authMiddleware) to line before app.use('/admin', adminRouter).
- 09:40Redeploy; test /admin/analytics – now returns 401 as expected.
I had been working on a new admin dashboard feature. The codebase had a modular structure: auth middleware in auth.js, admin routes in adminRoutes.js. In app.js, I mounted the admin router with app.use('/admin', adminRouter) and then, later, I had app.use(authMiddleware). The auth middleware was meant to protect all routes, but because it was registered after the admin router, requests to /admin/analytics never hit the auth middleware – they matched the admin router directly and were handled without any authentication.
I also discovered that the adminRouter had its own middleware for rate limiting, but no auth. I had assumed the global auth middleware would run first, but that's not how Express works – order of registration is everything. The fix was straightforward: move the auth middleware registration before any route-specific mounts. I also added a check to ensure auth middleware is always at the top of the stack.
The lesson: always define global middleware before any route mounts. Use a consistent pattern: first global middleware (logging, auth, etc.), then route mounts, then error handlers. I now also add a simple test that sends a request to every route and asserts the expected middleware side effects (like headers set by auth).
Root cause
Middleware registered after route mount – auth middleware placed after admin router, so /admin requests never passed through auth.
The fix
Moved app.use(authMiddleware) to before app.use('/admin', adminRouter) in app.js.
The lesson
Express middleware order is determined solely by registration order. Always register global middleware before any route-specific mounts.
Express uses a single middleware stack per application. When a request arrives, it walks through the stack in the order each middleware was registered via app.use(), app.get(), etc. For each middleware, Express checks if the request path matches the middleware's path (if provided). If it matches, the middleware function runs. If the middleware calls next(), Express proceeds to the next matching middleware. If next('route') is called, Express skips to the next route handler. If next(err) is called, Express jumps to the first error-handling middleware (with 4 parameters).
A common misconception is that middleware defined on a router runs in the order they are defined in that router, but relative to other routers, the order depends on where the parent router is mounted. For example, if you mount routerA with app.use('/api', routerA) and routerB with app.use('/', routerB), requests to /api/users will only go through routerA's middleware, not routerB's, even if routerB has a /api/users route. The path prefix determines which stack the request enters.
Express 4 does not handle promise rejections automatically. If an async middleware throws or rejects, the promise is unhandled and the request hangs indefinitely. The middleware stack stops because next() was never called. This often manifests as middleware 'not executing in order' because later middlewares never run. The fix is to wrap async functions in a try-catch and call next(err) on rejection, or use a wrapper function like asyncHandler.
Example of problematic async middleware: `app.use(async (req, res, next) => { const data = await fetchData(); // if fetchData rejects, next() is never called next(); })`. The correct pattern: `app.use(async (req, res, next) => { try { const data = await fetchData(); next(); } catch (err) { next(err); } })`.
Routes are also middleware functions. When you define `app.get('/users', handler)`, Express registers a middleware that matches GET /users. If you have a middleware `app.use('/users', someMiddleware)` and then `app.get('/users', handler)`, both will run for a GET /users request because the path matches. But if you define the route before the middleware, the route handler runs first and the middleware after it will also run (because route handlers typically call next() only if they don't send a response). However, if the route handler sends a response, subsequent middleware is skipped.
This behavior often confuses beginners: they expect middleware defined after a route to not execute for that route, but Express still runs them unless the route handler explicitly calls next()? Actually, if the route handler sends a response, the rest of the stack is skipped because the response is already sent. So if you have a middleware that should always run before the route, it must be defined before the route.
To see the exact order of middleware, you can use the `express-middleware-stack` package or simply log the stack by iterating over `app._router.stack`. Each layer has a `route` property (if it's a route handler) or `handle` (if it's middleware). You can print the path and function name to debug ordering. For example: `app._router.stack.forEach(layer => { if (layer.route) { console.log('Route:', layer.route.path, layer.route.methods); } else { console.log('Middleware:', layer.regexp); } })`. This gives you the exact order Express will use.
Another technique: temporarily add a middleware at the top that logs the request path and then call next(). This confirms that all requests enter the stack at the top. If you see a request missing that log, it means the server is handling it before even reaching your middleware (unlikely) or the request is not reaching your Node process at all (e.g., proxy issue).
Frequently asked questions
Does the order of app.use() calls matter if they are on different paths?
Yes, absolutely. Express checks middleware in the order they are registered, but only runs them if the request path matches the middleware's path (default is '/'). So if you have app.use('/admin', adminRouter) before app.use(authMiddleware), a request to /admin/dashboard will match adminRouter first and never hit authMiddleware because adminRouter likely handles the request. To ensure auth runs first, register authMiddleware before adminRouter.
Why is my error-handling middleware not catching errors?
Error-handling middleware must have exactly 4 parameters: (err, req, res, next). Also, it must be registered after all other routes and middleware. If you define it before a route that throws an error, the error will not be caught because Express only jumps to the next error-handling middleware in the stack – it does not go backwards. Finally, ensure that errors are passed to next via next(err) or thrown synchronously. Asynchronous errors must be caught and passed to next explicitly.
How do I run middleware only for specific HTTP methods?
Use app.METHOD() instead of app.use(). For example, app.get('/path', middleware) runs middleware only for GET requests. If you want the middleware to run for multiple methods, you can use app.all('/path', middleware). Alternatively, you can check req.method inside app.use() and call next() if it doesn't match.
Why does next() inside a setTimeout cause the request to hang?
Because setTimeout is asynchronous. If you call next() inside setTimeout, you must ensure that next() is called exactly once and that the middleware does not also call next() synchronously. The request hangs if next() is never called. Also, if the callback throws, it becomes an unhandled error. Always use try-catch inside async callbacks.
Can I reorder middleware at runtime?
No, not easily. The middleware stack is built at startup. You can manipulate app._router.stack, but that is undocumented and fragile. The correct approach is to restructure your code to use conditional logic within middleware (e.g., check req.path) to decide whether to run or skip.