Middleware Flow, next(), and Error-Handling Middleware
In Express, a request doesn't go straight to your route. It passes through a chain of functions first: logging, parsing the body, checking the login. These functions are middleware, and next() is what moves the request along the chain.
Definition
- Middleware: a function that runs between the request arriving and the response being sent. It receives
req,res, andnext. next(): a function that passes control to the next middleware in the chain.- Error-handling middleware: a special middleware with four arguments,
(err, req, res, next), that runs only when an error occurs.
Analogy: think of airport security. A passenger (request) goes through checkpoints in order: ticket check, ID check, baggage scan, and finally the gate (route handler). Each checkpoint can let the passenger through (next()), stop them (send a response), or send them to a special desk when something is wrong (next(err) to the error handler).
What a Middleware Can Do
Every middleware must do one of these:
| Action | How |
|---|---|
| Pass the request on | Call next() |
| End the request | Send a response: res.send(), res.json(), res.status(...).end() |
| Report an error | Call next(err) or throw an error |
Along the way, it can also run any code and change req and res, for example by adding req.user.
If a middleware does none of these, the request hangs until the client times out.
A Basic Middleware Chain
const express = require("express");const app = express();// 1. Logger: runs for every requestapp.use((req, res, next) => {console.log(`${req.method} ${req.url}`);next();});// 2. Body parser (built-in)app.use(express.json());// 3. Auth checkfunction requireAuth(req, res, next) {const token = req.headers.authorization;if (!token) {return res.status(401).json({ message: "Login required" });}req.user = { id: 1, name: "Asha" }; // pass data to later middlewarenext();}// 4. Route handler, protected by requireAuthapp.get("/profile", requireAuth, (req, res) => {res.json(req.user);});app.listen(3000);
For GET /profile with a token:
- Logger prints
GET /profileand callsnext(). express.json()parses the body (none here) and callsnext().requireAuthfinds the token, addsreq.user, and callsnext().- The route handler sends
req.useras JSON. The cycle ends.
Without a token, requireAuth sends a 401 response and does not call next(), so the route handler never runs.
Order Matters
Middleware runs in the order it is registered, top to bottom.
// Wrong: the route runs before express.json(), so req.body is undefinedapp.post("/users", (req, res) => {res.json(req.body); // undefined});app.use(express.json());// Right: parse the body firstapp.use(express.json());app.post("/users", (req, res) => {res.json(req.body);});
Where Middleware Can Be Attached
| Level | How | Runs for |
|---|---|---|
| Application-level | app.use(fn) | Every request |
| Path-level | app.use("/api", fn) | Requests starting with /api |
| Route-level | app.get("/profile", fn, handler) | Only that route |
| Router-level | router.use(fn) | Requests handled by that router |
Types of Middleware
| Type | Examples |
|---|---|
| Built-in | express.json(), express.urlencoded(), express.static() |
| Third-party | cors, helmet, morgan, cookie-parser |
| Custom | Your own logger, auth check, validation |
| Error-handling | (err, req, res, next) => { ... } |
The Ways to Call next()
| Call | What it does |
|---|---|
next() | Go to the next middleware or route handler |
next(err) | Skip all normal middleware and jump to the error-handling middleware |
next("route") | Skip the remaining handlers of this route and go to the next matching route. Works only in app.get(), app.post(), and similar, not in app.use() |
next("router") | Leave the current router and continue after it |
app.get("/users/:id",(req, res, next) => {if (req.params.id === "me") return next("route"); // skip to next routenext();},(req, res) => res.send("User " + req.params.id),);app.get("/users/:id", (req, res) => {res.send("Your own profile");});// GET /users/5 -> "User 5"// GET /users/me -> "Your own profile"
Common Mistakes With next()
1. Forgetting next()
app.use((req, res, next) => {console.log("Request received");// forgot next(): every request hangs here});
2. Code Keeps Running After next() or res.send()
next() and res.send() don't stop the function. The lines after them still run.
// Wrong: sends a response, then calls next(), and the route sends another onefunction requireAuth(req, res, next) {if (!req.headers.authorization) {res.status(401).send("Login required");}next();}// Error: Cannot set headers after they are sent to the client// Right: return after sendingfunction requireAuth(req, res, next) {if (!req.headers.authorization) {return res.status(401).send("Login required");}next();}
3. Expecting Code After next() to Run After the Response
Express does not wait for the rest of the chain to finish. To run code after the response is sent, listen for the finish event:
app.use((req, res, next) => {const start = Date.now();res.on("finish", () => {console.log(`${req.method} ${req.url} took ${Date.now() - start} ms`);});next();});
Error-Handling Middleware
An error-handling middleware has exactly four parameters. Express recognizes it by the number of parameters, so you must write all four, even if you don't use next.
app.use((err, req, res, next) => {console.error(err);res.status(err.statusCode || 500).json({message: err.message || "Something went wrong",});});
How an Error Reaches It
| Where the error happens | How it reaches the error handler |
|---|---|
Sync code in a handler (throw new Error(...)) | Automatically |
| Called manually | next(err) |
| Async handler in Express 5 (rejected promise) | Automatically |
| Async handler in Express 4 | You must catch it and call next(err) |
Inside a callback (like fs.readFile) | You must call next(err) yourself |
When an error is passed, Express skips every normal middleware and goes straight to the first error-handling middleware.
A Complete Example
const express = require("express");const app = express();app.use(express.json());// Custom error class with a status codeclass AppError extends Error {constructor(message, statusCode) {super(message);this.statusCode = statusCode;}}// Routesapp.get("/users/:id", async (req, res, next) => {try {const user = await findUser(req.params.id);if (!user) throw new AppError("User not found", 404);res.json(user);} catch (err) {next(err); // needed in Express 4, automatic in Express 5}});// 404 handler: a normal middleware placed after all routesapp.use((req, res, next) => {next(new AppError(`Route ${req.originalUrl} not found`, 404));});// Error handler: always lastapp.use((err, req, res, next) => {if (res.headersSent) {return next(err); // let Express's default handler close the connection}const status = err.statusCode || 500;res.status(status).json({message: status === 500 ? "Internal server error" : err.message,});});app.listen(3000);
Key points:
- The 404 handler is a normal middleware (3 parameters). It runs only if no route matched.
- The error handler goes last, after all routes and the 404 handler.
- If the response has already started (
res.headersSent), pass the error to Express's default handler instead of sending another response. - Hide internal details for 500 errors, so you don't leak stack traces to users.
Chaining Error Handlers
You can have more than one error handler. Call next(err) to pass the error to the next one:
// 1. Log every errorapp.use((err, req, res, next) => {console.error(err.stack);next(err);});// 2. Send the responseapp.use((err, req, res, next) => {res.status(err.statusCode || 500).json({ message: err.message });});
Summary
- Middleware is a function with
(req, res, next)that runs between the request and the response. - Each middleware must call
next(), send a response, or pass an error, or the request hangs. - Middleware runs in the order it is registered.
- It can be attached at the app, path, route, or router level.
next()moves on,next(err)jumps to the error handler,next("route")skips to the next matching route.- Use
returnwhen sending a response early, or you get "Cannot set headers after they are sent". - Error-handling middleware has four parameters,
(err, req, res, next), and goes last. - Sync errors reach it automatically. Async errors do too in Express 5, but need
next(err)in Express 4. - Put a 404 handler after all routes and before the error handler.
Interview Answer: How Does Middleware Work in Express?
Middleware is a function that receives req, res, and next, and runs between the request arriving and the response being sent. Express runs middleware in the order it is registered. Each one can run code, change req or res, and then must either call next() to pass control on, send a response to end the cycle, or pass an error. If it does none of these, the request hangs.
Calling next(err) skips all normal middleware and jumps to the error-handling middleware, which has four parameters, (err, req, res, next). Express identifies it by that parameter count. I place it last, after all routes and a 404 handler. Sync errors reach it automatically, and in Express 5, rejected promises from async handlers do too. In Express 4, I have to catch async errors and call next(err) myself.
A common mistake is sending a response without return and then calling next(), which causes "Cannot set headers after they are sent".