Eliminar el código genérico try/catch de las rutas de Express con asyncHandler
Aprenda cómo el middleware de errores, un envoltorio asyncHandler y una clase AppError personalizada trasladan las respuestas de errores de Express fuera de cada ruta y a un lugar consistente.
Fuentes: Stop Repeating try/catch: A Cleaner Error Handling Pattern in Express.js
La mayoría de las APIs Express comienzan con un try/catch en cada ruta asíncrona que convierte los fallos en una respuesta JSON. A medida que la API crece, ese bloque se copia en cada manejador. Aquí moverás las respuestas de error a un único middleware, eliminarás el repetido try/catch mediante un pequeño contenedor y señalarás los códigos de estado con una clase de error personalizada. Para los fallos a nivel de proceso, consulta nuestra guía sobre el manejo de errores de nivel producción en Node.js.
Por qué el try/catch por ruta no es escalable
Una primera versión suele envolver la lógica y enviar un 500 en caso de fallo.
app.get("/users", async (req, res) => {
try {
const users = await getUsers();
res.json(users);
} catch (err) {
res.status(500).json({ message: err.message });
}
});
No hay nada malo aquí hasta que esa estructura se repite en docenas de rutas.
app.get("/users", async (req, res) => {
try {
// ...
} catch (err) {
res.status(500).json({ message: err.message });
}
});
app.get("/products", async (req, res) => {
try {
// ...
} catch (err) {
res.status(500).json({ message: err.message });
}
});
app.post("/orders", async (req, res) => {
try {
// ...
} catch (err) {
res.status(500).json({ message: err.message });
}
});
Cada manejador ahora realiza dos tareas: la lógica de negocio y decidir cómo se mostrarán los errores en la comunicación. Cambiar el formato implica editar cada ruta.
Middleware de manejo de errores en Express
Express reconoce al middleware de errores por su aridad: una función con cuatro parámetros, donde el error viene primero.
(err, req, res, next)
Una versión mínima registra el error y devuelve un código 500 genérico. Regístrela después de sus rutas, ya que Express ejecuta los middleware en orden. Mantenga los cuatro parámetros incluso si next no se utiliza, ya que Express lo tratará como un middleware ordinario.
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({
message: "Internal Server Error"
});
});
Reenvío de errores con next(err)
En lugar de construir la respuesta dentro del bloque catch, la ruta transfiere el error a Express.
app.get("/users", async (req, res, next) => {
try {
const users = await getUsers();
res.json(users);
} catch (err) {
next(err);
}
});
next(err) hace que Express omita los middleware regulares y pase directamente al manejador de errores:
Request
↓
Route
↓
Business logic
↓
Error?
↓
next(err)
↓
Centralized error handler
↓
HTTP Response
Eliminación del try/catch restante
Cada ruta sigue conteniendo un try/catch. asyncHandler envuelve una función de ruta, la ejecuta mediante Promise.resolve y envía cualquier rechazo a next.
const asyncHandler = (fn) => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
La ruta se reduce a su lógica real:
app.get(
"/users",
asyncHandler(async (req, res) => {
const users = await getUsers();
res.json(users);
})
);
Cuando getUsers() rechaza, el wrapper llama a:
next(err);
En Express 5, los rechazos asíncronos se envían automáticamente a next, por lo que el wrapper es importante principalmente en Express 4.
Señalización de códigos de estado con AppError
Los errores son más útiles cuando incluyen un código de estado HTTP:
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
}
}
Las rutas lanzan errores significativos en lugar de formatear respuestas:
app.get(
"/users/:id",
asyncHandler(async (req, res) => {
const user = await getUser(req.params.id);
if (!user) {
throw new AppError("User not found", 404);
}
res.json(user);
})
);
El manejador lee statusCode, recurriendo a 500 si es necesario:
app.use((err, req, res, next) => {
console.error(err);
const statusCode = err.statusCode || 500;
res.status(statusCode).json({
success: false,
message: err.message || "Internal Server Error"
});
});
Un usuario ausente ahora genera un 404 con:
{
"success": false,
"message": "User not found"
}
Advertencia: mostrar err.message para cada error puede revelar información interna como mensajes de la base de datos. Devuélvalo solo para instancias de AppError y, en otros casos, un texto genérico. Para una estructura estándar de datos, consulte nuestra descripción general en detalles del problema RFC 9457.
Estructura completa de un proyecto
Una estructura sencilla:
src/
├── controllers/
│ └── user.controller.js
├── middleware/
│ ├── asyncHandler.js
│ └── errorHandler.js
├── errors/
│ └── AppError.js
├── routes/
│ └── user.routes.js
└── app.js
El módulo del wrapper
const asyncHandler = (fn) => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
module.exports = asyncHandler;
El módulo de la clase de error
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
}
}
module.exports = AppError;
El módulo del manejador de errores
const errorHandler = (err, req, res, next) => {
console.error(err);
res.status(err.statusCode || 500).json({
success: false,
message: err.message || "Internal Server Error"
});
};
module.exports = errorHandler;
Un controlador que utiliza ambos
Los controladores obtienen datos, lanzan AppError cuando es necesario y devuelven una estructura de éxito.
const asyncHandler = require("../middleware/asyncHandler");
const AppError = require("../errors/AppError");
const getUsers = asyncHandler(async (req, res) => {
const users = await userService.getUsers();
res.json({
success: true,
data: users
});
});
const getUser = asyncHandler(async (req, res) => {
const user = await userService.getUser(req.params.id);
if (!user) {
throw new AppError("User not found", 404);
}
res.json({
success: true,
data: user
});
});
module.exports = {
getUsers,
getUser
};
Conectarlo en app.js
El manejador se coloca al final.
const express = require("express");
const userRoutes = require("./routes/user.routes");
const errorHandler = require("./middleware/errorHandler");
const app = express();
app.use(express.json());
app.use("/users", userRoutes);
// Must be after routes
app.use(errorHandler);
module.exports = app;
Qué te aporta el patrón
Los controladores dejan de repetirse:
try {
// ...
} catch (err) {
res.status(500).json(...);
}
Un único formato para los errores
Los clientes pueden confiar en una sola estructura:
{
"success": false,
"message": "User not found"
}
Controladores enfocados
La tarea de un controlador se vuelve sencilla:
Get data
↓
Validate
↓
Process
↓
Return result
Un único lugar para la observabilidad
El registro, los IDs de solicitud y el seguimiento de errores se encuentran en un solo middleware.
El orden es importante: registra el manejador por último
Debe seguir a todos los enrutadores que cubre:
app.use("/users", userRoutes);
app.use("/products", productRoutes);
// Error handler LAST
app.use(errorHandler);
Si se registra antes, nunca verá los errores de las rutas posteriores.
Conclusión
En lugar de:
Route
└── try/catch
└── send error response
se convierte en:
Route
└── business logic
└── throw error
↓
asyncHandler
↓
centralized error handler
↓
consistent HTTP response
El objetivo real es separar la lógica de negocio de la presentación de errores:
- El middleware de errores necesita cuatro parámetros y debe registrarse por último.
asyncHandler en Express 4; Express 5 maneja automáticamente las rechazos asíncronos.AppError con un código de estado para los fallos esperados, y oculte los mensajes de aquellos inesperados.Lecturas relacionadas
- Equilibrar la generación CRUD de Prisma con control intencional de rutas — Aprenda cómo la generación basada en esquemas de routers CRUD de Prisma puede eliminar el código genérico repetitivo, manteniendo al mismo tiempo las decisiones relacionadas con la confianza, el alcance y la exposición en el código de la aplicación.
- Diseño de API en Node.js con capas: de controladores complejos a arquitectura limpiap — Aprenda cómo refactorizar una API de Node.js en capas de controlador, servicio y acceso a datos para solucionar la lógica empresarial enredada, los errores inconsistentes y las dificultades de escalado.
- Enviar y recibir SMS en Node.js con Twilio Webhooks y Express — Cree un pequeño servidor Express que envíe mensajes a través de Twilio, reciba respuestas mediante un webhook expuesto con ngrok y las responda automáticamente con TwiML.
- Truncación de cadenas segura con emojis en JavaScript usando Intl.Segmenter — Por qué la función slice() y la truncación basada en spread dañan los emojis y el texto con acentos, cómo se dañaron productos reales por esto, y cómo cortar cadenas en los límites de grafemas en su lugar.
- Validación de entrada en el lado servidor en Express con cadenas de express-validator — Entienda por qué las APIs de Express deben validar la entrada en el servidor, cómo las cadenas de express-validator registran errores para validationResult, y cómo estructurar una ruta para producción.