This is the general structure of my code:
(async () => {
try {
const asyncActions = []
for (let i = 0; i < 3; i++) {
await new Promise((resolve, reject) => setTimeout(resolve, 1000))
for (let j = 0; j < 3; j++) {
asyncActions.push(new Promise((resolve, reject) => setTimeout(reject, 1000)))
}
}
await Promise.all(asyncActions)
console.log('all resolved')
}
catch (e) {
console.log('caught error', e)
}
})()
I expect this to catch any rejections happening in asyncActions
because they should be handled by Promise.all()
, but somehow they are unhandled? The console shows the following:
(node:9460) UnhandledPromiseRejectionWarning: undefined
(Use `node --trace-warnings ...` to show where the warning was created)
(node:9460) 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: 1)
(node:9460) [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.
(node:9460) UnhandledPromiseRejectionWarning: undefined
(node:9460) 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)
...
(node:9460) PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)
(node:9460) PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 2)
...
Why are they not handled by Promise.all()
and then caught in the catch block?
I also noticed that when I replace both the new Promise(...)
with just Promise.resolve()
and Promise.reject()
respectively it catches the errors. Why is that? Aren't both variants asynchronous and thus should work the same way?
The way we detect non-handling of promise rejections in Node.js is using a heuristic.
When a promise is rejected we give the user a chance to still attach a listener (synchronously) - if they don't we assume it's not handled and cause an unhandledRejection
. This is because:
So - you need to always add the catch listeners synchronously in order to avoid the unhandled rejections.
You can also (in your non-contrived case) just opt out by adding an empty catch listener in a fork:
(async () => {
try {
const asyncActions = []
for (let i = 0; i < 3; i++) {
await new Promise((resolve, reject) => setTimeout(resolve, 1000))
for (let j = 0; j < 3; j++) {
const p = new Promise((resolve, reject) => setTimeout(reject, 1000));
p.catch(() => {}); // suppress unhandled rejection
asyncActions.push(p)
}
}
await Promise.all(asyncActions)
console.log('all fulfilled')
}
catch (e) {
console.log('caught error', e)
}
})()