Express.js
Manejo de errores en Express 5
Explica propagación de errores síncronos y asíncronos, middleware de cuatro argumentos, headersSent y manejo centralizado dentro de Express.js 5.
- Última actualización
- Actualizada
- Nivel
- Fundamentos
Express.js
Explica propagación de errores síncronos y asíncronos, middleware de cuatro argumentos, headersSent y manejo centralizado dentro de Express.js 5.
El error middleware es el último traductor del pipeline. Debe distinguir fallos esperados, fallos operativos e invariantes rotas sin filtrar detalles internos.
Express utiliza un pipeline especializado para errores. Una capa entra en él cuando:
next(error)app.use(errorHandler);El error handler se registra después de routers y 404.
import type { ErrorRequestHandler } from 'express';
export const errorHandler: ErrorRequestHandler = (
error,
request,
response,
next,
) => {
if (response.headersSent) {
next(error);
return;
}
// map, log and respond
};Los cuatro argumentos son necesarios para que Express lo trate como error middleware.
Sin manejo central, cada handler repite try/catch, produce formatos diferentes y puede exponer stack traces. Un punto central:
throw / rejection / next(error)
↓
Express salta middleware normal
↓
error middleware
├─ reconocer error esperado
├─ registrar fallo interno
├─ mapear contrato público
└─ responder o delegarapp.get('/orders/:id', async (request, response) => {
const order = await orders.findById(request.params.id);
response.json(order);
});Si findById rechaza, Express 5 llama al error pipeline automáticamente.
La Promise debe formar parte del valor retornado por el handler. Esto escapa:
app.post('/reports', async (_request, response) => {
generateReport().catch(console.error); // trabajo desprendido
response.sendStatus(202);
});Una tarea durable pertenece a una cola. Una tarea pequeña necesita su propio catch y observabilidad.
readFile(path, (error, data) => {
if (error) {
next(error);
return;
}
response.send(data);
});Un throw dentro de un callback posterior no siempre queda conectado a la Promise del handler. Propaga explícitamente.
JSON inválido, params o schema incorrectos. Son esperados y se responden 4xx.
Recurso ausente, conflicto de estado, stock insuficiente. Son parte del contrato de aplicación.
Timeout, pool agotado, upstream indisponible. Deben registrarse y quizá responder 503/504.
TypeError, invariantes rotas o ramas imposibles. Responden 500 y requieren investigación.
No confíes únicamente en error.message para clasificar; usa clases, códigos o discriminated unions.
function toProblem(error: unknown): HttpProblem {
if (error instanceof RequestValidationError) {
return {
status: 422,
code: 'VALIDATION_ERROR',
title: 'La entrada no cumple el contrato',
details: error.publicIssues,
};
}
if (error instanceof OrderNotFoundError) {
return { status: 404, code: 'ORDER_NOT_FOUND', title: 'Orden no encontrada' };
}
if (error instanceof DependencyTimeoutError) {
return {
status: 503,
code: 'DEPENDENCY_UNAVAILABLE',
title: 'Servicio temporalmente no disponible',
retryable: true,
};
}
return { status: 500, code: 'INTERNAL_ERROR', title: 'Error interno' };
}Registra internamente:
error.cause)No registres:
const problem = toProblem(error);
logger.error({ error, requestId }, 'Request failed');
response.status(problem.status).json({
type: `https://api.example.com/problems/${problem.code.toLowerCase()}`,
title: problem.title,
status: problem.status,
code: problem.code,
requestId,
details: problem.details,
});El cliente recibe código estable, no stack ni nombre de tabla.
Si un stream ya inició la response:
if (response.headersSent) {
next(error);
return;
}El handler predeterminado puede cerrar la conexión. Intentar res.status(500).json(...) produciría una segunda escritura.
Los parsers pueden lanzar antes de la ruta. Reconoce sus propiedades sin acoplar el contrato a internals frágiles. Mapea JSON malformado a 400 y body demasiado grande a 413.
Propagar el mismo error más de una vez puede activar handlers adicionales o el predeterminado. Cada rama debe tener una única salida.
No termines el proceso por cualquier error de request. Un fallo esperado solo afecta esa operación.
Errores globales no manejados (uncaughtException, unhandledRejection) pueden dejar el proceso en estado incierto. La estrategia habitual es registrar, dejar de aceptar tráfico, cerrar con gracia y permitir que un supervisor reinicie. No intentes continuar indefinidamente sin comprender el estado.
export const errorHandler: ErrorRequestHandler = (
error,
_request,
response,
next,
) => {
if (response.headersSent) {
next(error);
return;
}
const problem = toProblem(error);
const requestId = response.locals.requestId;
const log = problem.status >= 500 ? logger.error.bind(logger) : logger.warn.bind(logger);
log({ error, requestId, code: problem.code }, 'HTTP request failed');
response.status(problem.status).json({
title: problem.title,
status: problem.status,
code: problem.code,
requestId,
details: problem.details,
});
};La operación pudo completarse. No devuelvas un mensaje que invite a retry inseguro sin idempotencia.
Puede caer al handler predeterminado. Mantén el mapper simple y probado.
JavaScript permite throw 'oops'. Normaliza unknown y conserva información segura.
Registrar un error de escritura como 500 normal puede contaminar métricas. Distingue aborts.
Usa cause para conservar contexto técnico sin exponerlo públicamente.
try/catch idéntico en cada handler.headersSent.Prueba:
Clases de error facilitan mapping, pero pueden formar jerarquías rígidas. Result types hacen explícitos los errores esperados, pero añaden branching. Elige una convención y conserva unknown en boundaries.
headersSent limita la recuperación.headersSent es true?error.message?Modelo de errores de la API diseña el contrato público que consumidores pueden interpretar de forma estable.