-
Notifications
You must be signed in to change notification settings - Fork 74
v1 Unhandled promise rejections
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...
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)
- Production mode (
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.
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 everyasync
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 theasyncHandler
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.
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 });
}));