Skip to content

v1 Unhandled promise rejections

Jonathan Sharpe edited this page Aug 24, 2024 · 1 revision

Problem

Server

You may see problems with an "unhandled promise rejection" in your server code. In Node < 15 this will show an error in the server logs and warn that this is deprecated:

[server] (node:28274) UnhandledPromiseRejectionWarning: <error>
[server] (Use `node --trace-warnings ...` to show where the warning was created)
[server] (node:28274) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 2)
[server] (node:28274) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

In Node >= 15 (the "future" mentioned above) this will crash your app:

[server] [UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason <error>.] {
[server]   code: 'ERR_UNHANDLED_REJECTION'
[server] }
[server] [nodemon] app crashed - waiting for file changes before starting...

Client

Depending on your Node version and which mode the app is running in, from the client's perspective, you'll see one of the following:

  • Node < 15: the request just hangs forever, you'll see e.g. (pending) in the browser's Network tab
  • Node >= 15:
    • Production mode (npm run serve): the request fails entirely, you'll see e.g. (failed) in the Network tab
    • Development mode (npm run dev): the request returns 504 Gateway Timeout from the dev proxy (see Architecture#development-mode)

Likely reason

This can happen when you have an async Express handler function, like the following:

router.get("/endpoint", async (req, res, next) => {
  const result = await something();
  res.json({ result });
});

or if you use .then without .catch:

router.get("/endpoint", (req, res, next) => {
  something().then((result) => res.json({ result }));
});

If calling something() returns a promise that rejects, rather than resolving to the result, this error isn't handled anywhere.

Solutions

There are various ways ways to solve this, either:

  • Add .catch:

    router.get("/endpoint", (req, res, next) => {
      something().then((result) => res.json({ result })).catch((err) => next(err));
    });
  • Explicitly catch any errors in every async handler:

    router.get("/endpoint", async (req, res, next) => {
      try {
        const result = await something();
        res.json({ result });
      } catch (err) {
        next(err);
      }
    });
  • Recommended Create an asyncHandler wrapper as follows:

    const asyncHandler = (handler) => (req, res, next) => handler(req, res, next).catch((err) => next(err));

    (or use a pre-built one like express-async-handler) then wrap every async handler with this function:

    router.get("/endpoint", asyncHandler(async (req, res, next) => {
      const result = await something();
      res.json({ result });
    }));

    Per the Express docs, Express 5 will deal with rejected promises from async route handlers for you, at which point you'll be able to unwrap your handlers and delete the asyncHandler function.

In all cases, calling next(err) will let other middleware handle the error for you - by default, it will fall through to the logErrors middleware described in v1 Structure, which will log the error to the server console and respond 500 Internal Server Error to the client.

Specific responses

Note in either case, if you want to give a specific response rather than letting the error fall through to the logErrors middleware, you will need to catch the error in the handler:

router.get("/endpoint", asyncHandler(async (req, res, next) => {
  try {
    const result = await something();
    res.json({ result });
  catch (err) {
    res.sendStatus(404);  // not found
  }
}));

In these cases you can choose whether or not to apply the asyncHandler wrapper; it's generally recommended to use it on every async handler, so that you can be specific about which errors you want to catch and let the others fall through to the general middleware:

router.get("/endpoint", asyncHandler(async (req, res, next) => {
  let result;
  try {
    result = await something();  // if this fails we send a 404
  } catch (err) {
    return res.sendStatus(404);  // not found
  }
  const processedResult = await process(result);  // if this fails we log it and send 500
  res.json({ result: processedResult });
}));
Clone this wiki locally