How does error-handling middleware work in Express and how does the framework recognize it?
Show answer — try answering out loud first
An error middleware is a function with four parameters: (err, req, res, next). Express recognizes it as an error handler precisely because it declares four parameters, not because of its name. It is registered at the end, after all the normal routes and middleware. It is activated when at some point in the pipeline next(err) is called (passing it an error). In it you respond to the client with an error status (for example 500) and a JSON, without exposing the stack trace to the user.
In Express, errors are not handled one by one in each route. There is a central mechanism: when something goes wrong, instead of calling next() without arguments, you call next(err) passing it the error. That fact alone makes Express skip all the remaining normal middleware and routes, and jump straight to your error middleware.
How does Express know that a function is an error handler and not a normal middleware? By the number of parameters. A normal middleware has three: (req, res, next). An error one has four: (err, req, res, next). Express counts the function's parameters (using fn.length) and, if there are four, treats it as an error handler. That's why:
- The order and the number of parameters matter. Even if you don't use
nextinside, you must declare it so there are four. If you write only(err, req, res), Express will treat it as normal middleware and it will not work as an error handler. - It must go at the very end, after the routes. If you put it up top, it catches nothing, because errors travel "forward" in the pipeline.
Inside the handler you decide the response. What's recommended:
- Log the complete error on the server (with
console.erroror a logger) so you can see it in the logs. - Respond to the client with a generic message and an appropriate status (400, 404, 500...). Never send the
err.stackor internal details to the client: that leaks sensitive information about your server and is a security risk.
const express = require("express");
const app = express();
app.use(express.json());
// A route that triggers an error on purpose.
app.get("/reporte/:id", (req, res, next) => {
const id = Number(req.params.id);
if (Number.isNaN(id)) {
// Instead of responding here, we pass the error to the pipeline with next(err).
// Express will jump straight to the error middleware at the end.
return next(new Error("El id debe ser un número"));
}
res.json({ id, estado: "ok" });
});
// Route that simulates an unexpected failure.
app.get("/explota", (req, res, next) => {
try {
throw new Error("Algo falló en la base de datos");
} catch (err) {
next(err); // we send the error to the central handler
}
});
// ERROR MIDDLEWARE.
// It has FOUR parameters (err, req, res, next): that's why Express
// recognizes it as an error handler. It ALWAYS goes at the end.
app.use((err, req, res, next) => {
// 1) We log the complete error ONLY on the server (in the logs).
console.error("Error capturado:", err.message);
console.error(err.stack);
// 2) We respond to the client with something generic, WITHOUT the stack trace.
res.status(500).json({
error: "Ocurrió un error en el servidor",
});
});
app.listen(3000, () => {
console.log("Servidor en http://localhost:3000");
});
Declaring the error handler with only three parameters, like (err, req, res), removing the next "because I don't use it". By doing so, Express counts three parameters and treats it as a normal middleware, not as an error handler, so it never catches errors. Even if you're not going to use next, you have to declare it so there are four. The other serious mistake is sending the client the raw err.stack or err.message: that exposes file paths, versions and internal server details, and is an information leak.
"The error handler in Express is a special middleware with four parameters:
(err, req, res, next). Express distinguishes it from a normal middleware precisely by having four parameters instead of three; internally it checks the function's arity. I register it at the very end, after the routes. It fires when anywhere in the pipeline I callnext(err)with an error: there Express skips the rest and lands in this handler. Inside, I log the complete error in the server logs so I can debug it, but to the client I only return a status like 500 and a generic message in JSON. I never expose the stack trace to it, because that would leak internal details and would be a security problem."
Why does this error handler catch nothing, even though a route calls next(err)?
app.use((err, req, res) => {
res.status(500).json({ error: "Falló el servidor" });
});
See answer
Because it only has three parameters: (err, req, res). Express identifies error handlers by having four parameters (counting fn.length). With three, Express treats it as a normal middleware and does not invoke it when next(err) is called. The solution is to add the fourth parameter even if it's not used: app.use((err, req, res, next) => { ... }).