I have a fundamental question about how promises work in Node.js (in browsers the behaviour is as expected), consider the following function as an example:
async function proc(): Promise<void> {
const resolve = new Promise((resolve) => setTimeout(resolve, 0))
const reject = Promise.reject('reject')
console.log(await resolve)
try {
await reject
} catch (err) {
console.error(err)
}
}
Since the reject gets dequeued faster, it is thrown at await resolve
and since it is not handled there, we get an unhandled rejection.
Although there are plenty of solutions to work around this, I find this behaviour counter-intuitive. Is there any reason why the object passed to reject()
is not getting thrown at the time the corresponding promise is await
ed, similarly how the object passed to resolve()
is returned at the time of await
?
I expected await
to work something like this:
await(promise) {
let result, error;
promise
.then((data) => result = data)
.catch((err) => error = err)
// wait for promise magic
if (error != null) {
throw error
}
return result
}
CodePudding user response:
This has more to do with how NodeJS responds to "unhandled promise rejections," of which the default recently changed to throw
in Node 15.
Whenever an unhandled [promise] rejection occurs, the following steps are taken (docs):
- Emit
unhandledRejection
. - If this hook is not set, raise the unhandled rejection as an uncaught exception.
Therefore if you have the following script:
// example.js
async function proc() {
const resolve = new Promise((resolve) => setTimeout(resolve, 0));
const reject = Promise.reject("reject");
await resolve;
console.log("Able to `await resolve`");
try {
await reject;
} catch (err) {
console.error('Caught an error!');
console.error(err);
}
}
proc();
That result of executing that script changes depending on what mode
the --unhandled-rejections
flag is set to.
If it is set it to throw
(the default), you see that the resolved promise is never await
'd because the rejected promise throws before it can get to that line.
Also note that if you had set process.on('unhandledRejection', ...)
beforehand, you also wouldn't see the rejected promise throw.
Here is what you are probably seeing when you run the example.js script:
$ node --unhandled-rejections=throw example.js
node:internal/process/promises:279
triggerUncaughtException(err, true /* fromPromise */);
^
[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 "reject".] {
code: 'ERR_UNHANDLED_REJECTION'
}
However, if you run it with the mode set to warn
, you'll see something like this:
$ node --unhandled-rejections=warn example.js
(node:55979) UnhandledPromiseRejectionWarning: reject
(Use `node --trace-warnings ...` to show where the warning was created)
(node:55979) 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)
Able to `await resolve`
Caught an error!
reject
(node:55979) PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)
That is, we are able to get to the try
block since the rejected promise doesn't throw an uncaught exception.