Inicio / Artículos

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.

Editor: REACTAPP.TOP5 min de lectura1231 palabras

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.
  • Use asyncHandler en Express 4; Express 5 maneja automáticamente las rechazos asíncronos.
  • Lance AppError con un código de estado para los fallos esperados, y oculte los mensajes de aquellos inesperados.
  • Lecturas relacionadas