Most developers have typed app.use(express.json()) dozens of times. But if you asked what it actually does to a request, the answer is usually vague: “it parses the body” or “it runs before the route.” That’s not wrong, but it skips the part that matters for debugging.
Middleware is where most silent failures in Express apps live. A request hangs with no error. An auth check gets skipped. A body parser runs after the route that needs it. These bugs are hard to trace because they don’t throw. They just quietly produce wrong behavior.
The Express routing blog on this site covers where middleware fits in the routing layer. This blog goes one level deeper: what middleware actually is mechanically, how the execution model works, and how to write middleware you can reuse across projects without copying logic everywhere.
What middleware actually is
In Express, middleware is any function with this signature:
Three parameters, and each one has a specific role:
- req: the incoming request object. Headers, body, params, query — it’s all here.
- res: the response object. Use it to respond to the client.
- next: a function provided by Express. Call it to pass control to the next function in the stack. Don’t call it, and the request stops.
Express maintains an internal stack of functions. Every time you call app.use() or router.use(), you’re pushing a function onto that stack. When a request arrives, Express walks the stack from top to bottom, calling each function in order.
That’s the whole model. Everything else is a consequence of it.

The request pipeline: what happens step by step
When an HTTP request hits your Express app, here’s the sequence:
1. Express receives the request and wraps it in req and res objects.
2. It starts walking the middleware stack from the first registered function.
3. Each function either calls next() to continue, sends a response to end the pipeline, or calls next(err) to trigger error handling.
4. The first function that sends a response ends the chain; nothing after it runs.
Here’s that flow in code:
The log output is: first, second. The third function never executes because the second already sent a response.
This is not a bug. It’s the intended behavior. When you understand it, you can use it deliberately: short-circuit the pipeline early to block unauthorized requests, invalid payloads, or rate-limited clients before they reach your route handlers.
Let’s have a look at the visual where a horizontal pipeline has three stages. Version A: all three call next() and the request reaches the route handler. Version B: stage two sends a response, and the pipeline ends there.

Execution order: where most bugs live
Middleware runs in the order it is registered. There are no exceptions and no priority system. The three bugs below all share the same root cause: something was registered in the wrong order.
Bug 1: Body parser registered after the route that reads req.body
❌ Wrong — req.body is undefined
✅ Correct — parser runs before all routes
Bug 2: Auth middleware registered after the protected route
❌ Wrong — auth check never runs
✅ Correct — auth runs before the route
Bug 3: 404 handler registered before routes
❌ Wrong — every request returns 404
✅ Correct — 404 handler goes last
The pattern is the same in all three cases: anything that needs to run before a handler must be registered before that handler. Read your app.js file top to bottom, and you’ll see exactly the order Express sees.
Error-handling middleware
Error middleware has a four-argument signature. That fourth argument, err, is what makes Express treat a function as an error handler rather than regular middleware:
It only runs in two situations: when a synchronous handler throws, or when any middleware calls next(err) with a value. If next() is called without an argument, the error handler is skipped entirely.
The error handler must be registered last, after all routes. That’s not conventional. Express won’t recognize it as an error handler if it’s placed mid-stack.
A production-ready error handler
Most teams end up with a centralized error handler that distinguishes between two categories: operational errors (expected failures like 404, 401, 422) and unexpected failures (500). The cleanest way to handle this is a custom error class:
With that in place, the error handler becomes simple:
When operational errors are thrown anywhere in the app, they carry a statusCode that propagates to the client. Unexpected errors fall through to the default 500 message without leaking internal details.
The async gotcha in Express 4
In Express 4, if an async route handler throws or rejects, the error does not reach the error handler automatically. The process either crashes or hangs, depending on the Node.js version.
❌ Wrong — unhandled rejection in Express 4
✅ Correct — pass errors to next()
Express 5 fixes this. Rejected promises in async handlers are forwarded to the error handler automatically. If you’re on Express 4, the try/catch pattern above is the reliable option.
Writing reusable middleware
Most middleware tutorials show functions defined once and used once. That works for app-wide concerns like logging, but it doesn’t scale when you need the same middleware to behave differently in different contexts.
The solution is a middleware factory: a function that accepts configuration and returns a middleware function. This is how express.json(), cors(), and virtually every well-written third-party package work internally.
Example 1: A configurable request logger
A basic logger that accepts a log level, so you can silence it in tests or verbose it in development:
Example 2: A role-based access guard
An auth guard that accepts an array of allowed roles. The route handler stays clean and the access logic stays testable in isolation:
The handler deleteUser doesn’t know anything about authorization. It just runs if the request gets through. The requireRole middleware owns that responsibility, and you can test it independently by passing a mock req object with different roles.
Attaching data to req for downstream handlers
Middleware can attach data to the req object so downstream handlers don’t have to repeat work. The most common pattern is auth middleware that decodes a token and attaches the user:
One token decode, many handlers that can use the result.
Common mistakes
Calling next() after res.send()
❌ Wrong — causes ‘Cannot set headers after they are sent’
✅ Correct — return early to stop execution
Passing a plain Error to next() and losing the status code
❌ Wrong — status code is lost, client always gets 500
✅ Correct — use AppError to preserve the status
Async middleware without try/catch in Express 4
❌ Wrong — rejected promise is unhandled
✅ Correct — catch and forward to error handler
Registering the error handler mid-stack
❌ Wrong — error handler placed before routes
✅ Correct — error handler goes last
Quick-reference table
| Scenario | Recommended approach |
| Parse request bodies | app.use(express.json()) before all routes |
| Authenticate every request | App-level auth middleware, registered early |
| Protect a specific route group | Router-level middleware on that router |
| Validate request shape | Route-level middleware on individual routes |
| Handle all errors in one place | Four-argument error handler, registered last |
| Pass data between middleware | Attach to req object (e.g., req.user) |
| Async error handling in Express 4 | try/catch with next(err) in every async handler |
Wrapping up
The routing post covered how Express connects a request to a handler. This blog covered what happens in between. Middleware is just a stack of functions, and Express walks it in registration order. That’s the whole model.
Once that’s clear, most middleware bugs become obvious before they happen. You can read app.js from top to bottom, trace the execution path, and know exactly what runs and when.
Two habits to take from this: register app-level middleware before any routes that depend on it, and build shared logic as middleware factories rather than duplicating it across handlers. Both are small changes that pay off quickly as an app grows.