← Node & Express

Middleware

En Express 4, ¿qué pasa si un handler `async` lanza un error o su promesa se rechaza, y cómo lo manejas correctamente?

Ver respuesta — intenta responderla en voz alta primero

En Express 4, si un handler async lanza un error o su promesa se rechaza, Express no lo captura automáticamente: la petición se queda colgada. Esto pasa porque Express 4 no hace await de tu handler, así que el rechazo nunca llega a su manejador de errores. La solución es capturar el error con try/catch y llamar a next(err), o mejor aún, usar un wrapper asyncHandler que envuelve el handler y hace Promise.resolve(fn(...)).catch(next). En Express 5 esto ya no es necesario: captura los rechazos de promesas automáticamente.

Cuando escribes una ruta normal (síncrona) y lanzas un error, Express 4 lo atrapa y lo manda a tu middleware de errores. Pero con funciones async la historia cambia.

Una función async siempre devuelve una promesa. Si dentro de ella lanzas un error (o un await falla), esa promesa se rechaza. El problema es que Express 4 llama a tu handler así, en pseudocódigo:

handler(req, res, next); // NO hace await, NO mira el valor devuelto

Como Express 4 no hace await ni observa la promesa devuelta, ese rechazo se convierte en un "unhandled promise rejection" que Express nunca ve. Resultado: la petición no responde y se cuelga, y el cliente espera indefinidamente.

Hay dos formas de arreglarlo:

  • try/catch manual: envuelves el cuerpo del handler en try/catch y en el catch llamas a next(err). Funciona, pero tienes que repetir el try/catch en cada ruta async, lo cual es tedioso y fácil de olvidar.
  • Wrapper asyncHandler (la forma recomendada): una función que envuelve tu handler y captura cualquier rechazo por ti, llamando a next automáticamente. Así escribes tus rutas limpias, sin try/catch, y el error igual llega al manejador central.

El wrapper se basa en Promise.resolve(fn(...)).catch(next): toma lo que devuelva tu handler (una promesa), y si se rechaza, pasa el error a next, que lo lleva a tu middleware de errores.

Detalle importante para la entrevista: en Express 5 este problema desaparece. Express 5 hace await de los handlers y captura los rechazos de promesas automáticamente, redirigiéndolos al manejador de errores. Pero mientras trabajes con Express 4 (todavía muy común), necesitas el try/catch o el asyncHandler.

const express = require("express");
const app = express();

// Simulamos una operación async que puede fallar.
function buscarUsuario(id) {
  return new Promise((resolve, reject) => {
    if (id === "0") return reject(new Error("Usuario no encontrado"));
    resolve({ id, nombre: "Ana" });
  });
}

// PROBLEMA (Express 4): este handler async se cuelga si la promesa se rechaza.
// Express 4 no hace await, así que el rechazo nunca llega a next().
app.get("/malo/:id", async (req, res) => {
  const usuario = await buscarUsuario(req.params.id); // si rechaza -> petición colgada
  res.json(usuario);
});

// SOLUCIÓN 1: try/catch manual llamando a next(err).
app.get("/manual/:id", async (req, res, next) => {
  try {
    const usuario = await buscarUsuario(req.params.id);
    res.json(usuario);
  } catch (err) {
    next(err); // el error viaja al middleware de errores
  }
});

// SOLUCIÓN 2 (recomendada): wrapper asyncHandler.
// Envuelve el handler y captura cualquier rechazo, pasándolo a next.
function asyncHandler(fn) {
  return (req, res, next) => {
    // Promise.resolve normaliza el resultado a promesa;
    // .catch(next) manda cualquier error al pipeline de errores.
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}

// Rutas limpias, sin try/catch: el wrapper hace el trabajo.
app.get(
  "/bueno/:id",
  asyncHandler(async (req, res) => {
    const usuario = await buscarUsuario(req.params.id);
    res.json(usuario);
  })
);

// Middleware de errores central (cuatro parámetros), al final.
app.use((err, req, res, next) => {
  console.error("Error async:", err.message);
  res.status(500).json({ error: "Ocurrió un error en el servidor" });
});

app.listen(3000, () => {
  console.log("Servidor en http://localhost:3000");
});

Suponer que como una ruta síncrona que lanza un error sí lo maneja Express, una ruta async que lanza (o cuyo await falla) también lo hará. En Express 4 no es así: al no hacer await del handler, el rechazo de la promesa se pierde y la petición se cuelga sin responder. El síntoma es una ruta que "a veces no contesta" y un warning de UnhandledPromiseRejection en la consola. La solución es envolver todas las rutas async con try/catch + next(err) o con un asyncHandler, y no confiar en que Express 4 las capture solo.

"En Express 4, si un handler async lanza o su promesa se rechaza, Express no lo captura, porque no hace await de mi handler ni observa la promesa que devuelve; el rechazo se pierde y la petición se cuelga sin responder. La solución básica es un try/catch dentro del handler que llame a next(err) para mandar el error al middleware de errores. Pero como repetir eso en cada ruta es propenso a olvidos, prefiero un wrapper asyncHandler que envuelve el handler y hace Promise.resolve(fn(req, res, next)).catch(next), así mis rutas quedan limpias y cualquier rechazo llega solo al manejador central. Vale aclarar que en Express 5 esto ya no hace falta, porque sí captura los rechazos de promesas automáticamente."

Reto rápido

En Express 4, ¿por qué esta ruta deja al cliente esperando para siempre en lugar de responder con un error 500?

app.get("/pago", async (req, res) => {
  const resultado = await cobrarTarjeta(); // esta promesa se rechaza
  res.json(resultado);
});
Ver respuesta

Porque cobrarTarjeta() se rechaza y ese rechazo hace que la promesa devuelta por el handler async se rechace también. En Express 4, Express no hace await de tu handler ni observa la promesa que devuelve, así que ese rechazo se convierte en un "unhandled promise rejection" que Express nunca ve. Como nadie llamó a res.json, res.send ni a next(err), la petición queda colgada y el cliente espera indefinidamente. Se arregla con try/catch + next(err), o envolviendo el handler con un asyncHandler. (En Express 5 sí respondería con el error automáticamente.)