Express maneja los errores con un middleware especial que recibe cuatro argumentos, (err, req, res, next), y se registra después de todas tus rutas. Express le envía cada error lanzado dentro de una ruta, cada error que pasas a next(err) y, en Express 5, cada promesa rechazada de un handler async. El handler decide el status code y envía una única respuesta de error, así que el resto de tu código puede limitarse a lanzar errores.
GET /users/99 responde 404 con {"error":"No user with id 99"}, y la Console muestra la línea del error handler. Envía GET /users/1 y la ruta responde con normalidad; el error handler solo se ejecuta cuando algo sale mal.
Errores que Express captura por ti
Hay tres formas de llegar al error handler desde una ruta, y en Express 5 las tres funcionan igual:
GET /async responde 500 con {"error":"Rejected in an async route"}; GET /sync y GET /next acaban en el mismo handler con sus propios mensajes. Ahora quita next de los parámetros del error handler y vuelve a enviar GET /sync: con tres parámetros Express lo trata como un middleware normal, se lo salta y responde con su página de error HTML por defecto.
Errores async: Express 5 vs Express 4
La ruta /async funciona porque Express 5 captura la promesa rechazada. Express 4 no lo hacía: el error se convertía en un unhandled rejection y la petición se quedaba colgada.
| Express 4 | Express 5 | |
|---|---|---|
throw en un handler síncrono | Capturado | Capturado |
next(err) | Capturado | Capturado |
Promesa rechazada de un handler async | No capturado, la petición se cuelga | Capturado |
Error en un setTimeout o en un callback de evento | No capturado, el proceso se cae | No capturado, el proceso se cae |
En Express 4, envuelve cada handler async o instala el paquete express-async-errors:
// Express 4 only
const asyncHandler = (fn) => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await db.findUser(req.params.id);
res.json(user);
}));
La última fila es igual en las dos versiones: Express no puede ver un error lanzado en un callback que no llamó él. Convierte las APIs con callbacks en promesas (casi todo Node tiene una versión con promesas, como fs/promises) y haz await de ellas.
Un handler 404 y un error JSON coherente
Una API es más fácil de usar cuando todos los errores tienen la misma forma. Dale a los errores un status, convierte las rutas que no coinciden en un error 404 y deja que un solo handler les dé formato a todos:
GET /nope no coincide con ninguna ruta, así que el handler 404 lo convierte en un error y la respuesta es {"error":"Cannot GET /nope","status":404}. Envía GET /users/99 y el error que lanza la ruta recibe la misma forma. Envía POST /users con el body {"name": }: express.json() no consigue parsearlo y llama a next() con un error cuyo status es 400, así que la respuesta es {"error":"Unexpected token '}', \"{\"name\": }\" is not valid JSON","status":400}.
No filtres stack traces
Los mensajes de error de tu propio HttpError están escritos para el cliente. Los mensajes y stacks de todo lo demás (un driver de base de datos, una errata que lanza TypeError) pueden revelar rutas de archivos, consultas y versiones de librerías. El handler de arriba envía Internal server error para todos los 5xx y registra el error real en el servidor. if (res.headersSent) return next(err); cubre un error que ocurre cuando ya se ha enviado parte de la respuesta: lo único seguro en ese caso es dejar que Express cierre la conexión.
El handler por defecto de Express sigue NODE_ENV: incluye la stack trace en su página HTML salvo que NODE_ENV sea production. Defínelo en tu servidor de producción (NODE_ENV=production node server.js), pero no dependas de ello: un handler propio que nunca envía err.stack es más seguro.
Errores comunes
Registrar el error handler antes de las rutas. Express solo pasa los errores hacia delante, así que un handler que está por encima de una ruta nunca ve los errores de esa ruta. Ponlo al final, después del handler 404.
Capturar un error y no hacer nada con él. Un try/catch que solo registra el error deja la petición colgada. En el catch, llama a next(err), vuelve a lanzarlo o envía una respuesta.
Enviar dos respuestas. Lanzar un error después de res.json() hace que el error handler intente enviar de nuevo. Comprueba res.headersSent, como en el ejemplo.
Para ver cómo funcionan el middleware normal y next(), consulta middleware en Express.
Preguntas frecuentes
¿Puedo tener más de un error handler en Express?
Sí. Se ejecutan en orden, y cada uno envía una respuesta o llama a next(err) para pasar el error. Una división habitual es un handler que registra el error y otro que envía la respuesta.
¿Qué pasa si un error handler lanza un error?
Express pasa el nuevo error al siguiente error handler y, si no hay ninguno, al suyo integrado, que envía una página 500. Mantén los error handlers simples para que no puedan fallar ellos mismos.
¿Qué status code debe usar una respuesta de error?
Usa 4xx cuando el cliente envió algo mal (400 entrada no válida, 401 sin sesión iniciada, 403 sin permiso, 404 no encontrado, 409 conflicto) y 500 cuando falló tu código. Nunca respondas 200 con un error en el body.
¿Cómo devuelvo errores de validación de varios campos?
Responde 400 con una lista, por ejemplo { "errors": [{ "field": "email", "message": "is required" }] }. Con zod, transforma result.error.issues a esa forma en el error handler para que todas las rutas informen igual de la entrada no válida.