On this page

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, and next.
  • 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:

ActionHow
Pass the request onCall next()
End the requestSend a response: res.send(), res.json(), res.status(...).end()
Report an errorCall 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 request
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
// 2. Body parser (built-in)
app.use(express.json());
// 3. Auth check
function 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 middleware
next();
}
// 4. Route handler, protected by requireAuth
app.get("/profile", requireAuth, (req, res) => {
res.json(req.user);
});
app.listen(3000);

For GET /profile with a token:

  1. Logger prints GET /profile and calls next().
  2. express.json() parses the body (none here) and calls next().
  3. requireAuth finds the token, adds req.user, and calls next().
  4. The route handler sends req.user as 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 undefined
app.post("/users", (req, res) => {
res.json(req.body); // undefined
});
app.use(express.json());
// Right: parse the body first
app.use(express.json());
app.post("/users", (req, res) => {
res.json(req.body);
});

Where Middleware Can Be Attached

LevelHowRuns for
Application-levelapp.use(fn)Every request
Path-levelapp.use("/api", fn)Requests starting with /api
Route-levelapp.get("/profile", fn, handler)Only that route
Router-levelrouter.use(fn)Requests handled by that router

Types of Middleware

TypeExamples
Built-inexpress.json(), express.urlencoded(), express.static()
Third-partycors, helmet, morgan, cookie-parser
CustomYour own logger, auth check, validation
Error-handling(err, req, res, next) => { ... }

The Ways to Call next()

CallWhat 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 route
next();
},
(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 one
function 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 sending
function 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 happensHow it reaches the error handler
Sync code in a handler (throw new Error(...))Automatically
Called manuallynext(err)
Async handler in Express 5 (rejected promise)Automatically
Async handler in Express 4You 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 code
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
}
}
// Routes
app.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 routes
app.use((req, res, next) => {
next(new AppError(`Route ${req.originalUrl} not found`, 404));
});
// Error handler: always last
app.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 error
app.use((err, req, res, next) => {
console.error(err.stack);
next(err);
});
// 2. Send the response
app.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 return when 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".

Next TopicRouting and Request Validation (Joi/Zod)