← Node & Express

Middleware

In Express 4, what happens if an `async` handler throws an error or its promise is rejected, and how do you handle it correctly?

Show answer — try answering out loud first

In Express 4, if an async handler throws an error or its promise is rejected, Express does not catch it automatically: the request stays hung. This happens because Express 4 does not await your handler, so the rejection never reaches its error handler. The solution is to catch the error with try/catch and call next(err), or better still, use an asyncHandler wrapper that wraps the handler and does Promise.resolve(fn(...)).catch(next). In Express 5 this is no longer necessary: it catches promise rejections automatically.

When you write a normal (synchronous) route and throw an error, Express 4 catches it and sends it to your error middleware. But with async functions the story changes.

An async function always returns a promise. If inside it you throw an error (or an await fails), that promise is rejected. The problem is that Express 4 calls your handler like this, in pseudocode:

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

Since Express 4 does not await nor observe the returned promise, that rejection becomes an "unhandled promise rejection" that Express never sees. Result: the request does not respond and hangs, and the client waits indefinitely.

There are two ways to fix it:

  • Manual try/catch: you wrap the body of the handler in try/catch and in the catch you call next(err). It works, but you have to repeat the try/catch in every async route, which is tedious and easy to forget.
  • asyncHandler wrapper (the recommended way): a function that wraps your handler and catches any rejection for you, calling next automatically. This way you write your routes clean, without try/catch, and the error still reaches the central handler.

The wrapper is based on Promise.resolve(fn(...)).catch(next): it takes whatever your handler returns (a promise), and if it is rejected, it passes the error to next, which carries it to your error middleware.

Important detail for the interview: in Express 5 this problem disappears. Express 5 does await handlers and catches promise rejections automatically, redirecting them to the error handler. But as long as you work with Express 4 (still very common), you need the try/catch or the asyncHandler.

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

// We simulate an async operation that can fail.
function buscarUsuario(id) {
  return new Promise((resolve, reject) => {
    if (id === "0") return reject(new Error("Usuario no encontrado"));
    resolve({ id, nombre: "Ana" });
  });
}

// PROBLEM (Express 4): this async handler hangs if the promise is rejected.
// Express 4 does not await, so the rejection never reaches next().
app.get("/malo/:id", async (req, res) => {
  const usuario = await buscarUsuario(req.params.id); // if it rejects -> hung request
  res.json(usuario);
});

// SOLUTION 1: manual try/catch calling 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); // the error travels to the error middleware
  }
});

// SOLUTION 2 (recommended): asyncHandler wrapper.
// It wraps the handler and catches any rejection, passing it to next.
function asyncHandler(fn) {
  return (req, res, next) => {
    // Promise.resolve normalizes the result to a promise;
    // .catch(next) sends any error to the error pipeline.
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}

// Clean routes, without try/catch: the wrapper does the work.
app.get(
  "/bueno/:id",
  asyncHandler(async (req, res) => {
    const usuario = await buscarUsuario(req.params.id);
    res.json(usuario);
  })
);

// Central error middleware (four parameters), at the end.
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");
});

Assuming that because Express does handle an error thrown by a synchronous route, an async route that throws (or whose await fails) will also be handled. In Express 4 that is not the case: since it does not await the handler, the promise rejection is lost and the request hangs without responding. The symptom is a route that "sometimes doesn't answer" and an UnhandledPromiseRejection warning in the console. The solution is to wrap all async routes with try/catch + next(err) or with an asyncHandler, and not rely on Express 4 catching them on its own.

"In Express 4, if an async handler throws or its promise is rejected, Express does not catch it, because it does not await my handler nor observe the promise it returns; the rejection is lost and the request hangs without responding. The basic solution is a try/catch inside the handler that calls next(err) to send the error to the error middleware. But since repeating that in every route is prone to being forgotten, I prefer an asyncHandler wrapper that wraps the handler and does Promise.resolve(fn(req, res, next)).catch(next), so my routes stay clean and any rejection reaches the central handler on its own. It's worth clarifying that in Express 5 this is no longer needed, because it does catch promise rejections automatically."

Quick challenge

In Express 4, why does this route leave the client waiting forever instead of responding with a 500 error?

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

Because cobrarTarjeta() is rejected and that rejection causes the promise returned by the async handler to be rejected too. In Express 4, Express does not await your handler nor observe the promise it returns, so that rejection becomes an "unhandled promise rejection" that Express never sees. Since nobody called res.json, res.send or next(err), the request stays hung and the client waits indefinitely. It is fixed with try/catch + next(err), or by wrapping the handler with an asyncHandler. (In Express 5 it would respond with the error automatically.)