Reputation: 121
async function delay(ms) {
return new Promise(r=>setTimeout(r,ms))
}
async function fail(ms){
await delay(ms)
throw new Error("kek");
}
async function ok(ms){
await delay(ms)
return 1;
}
async function start() {
try{
let fail_p = fail(500);
let ok_p = ok(1000);
console.log(await ok_p)
console.log(await fail_p)
}
catch(e){
console.log("ERR")
}
}
start().then(()=>console.log("Finish"))
In the browser I get what I expected.
1
ERR
Finish
But in nodejs the application just crashes with the error "kek"
/path/test.js:6
throw new Error("kek");
^
Error: kek
at fail (/path/test.js:6:11)
Node.js v22.14.0
Is this a nodejs bug?
And how can I achieve the same behavior with nodejs?
Upvotes: 12
Views: 631
Reputation: 675
You experience an error because you don't handle a rejection of the promise:
start().then(() => console.log("Finish"))
Browsers should report an error here. For me, I get Uncaught (in promise) Error: kek
. As I'm sure seems obvious, an error should be reported if a promise rejects and you don't .catch()
it.
When running the code snippet using Stack Overflow, this error is only shown in the browser's console— not Stack Overflow's.
Upvotes: 1
Reputation: 351084
Both in browsers as in Node, there is an unhandled promise rejection, but they deal differently with it:
Node will deal with this according to the --unhandled-rejections=mode
flag. In your case the default action was taken, which is to throw the unhandled rejection as an uncaught exception. And by default, Node will exit when an uncaught exception event is emitted. See Event: 'uncaughtException'.
A browser will also raise this error, which appears in the console log. However, in a browser context, the JavaScript main thread and the host's job queues remain operational. As soon as a rejection handler has been attached to the rejected promise, the browser's console will typically remove this error from the console log.
One may wonder why there is an unhandled rejection in the first place, since there is a try...catch
block. But realise that the throw
that gets executed in the fail
function translates into the rejection of a promise, because that throw
executes in an async
function which has implicit handlers for errors. The state of the promise that is referenced by fail_p
changes to rejected and there is no more error. A try...catch
block does not deal with rejected promises; it deals with errors, but there are none.
What does raise an error is when an await
operator is applied to a promise that is (or will be) rejected. But in your scenario the await fail_p
expression did not execute yet because execution was suspended at the await ok_p
. That promise is still pending at the time that fail_p
rejects, and so we have a case where a rejected promise is not handled.
Unless the process exits (which is the case in Node), that unfortunate situation ends as soon as ok_p
fulfills, because then execution resumes to execute await fail_p
, and it is at that time a new error is thrown (with the same description as the promise rejection reason), and it is at that time the try...catch
block can do its error handling job. Now we no longer have an unhandled promise rejection anymore. But again, Node has already exited before ok_p
fulfilled, (with default Node settings), when it found that fail_p
rejected and there was no handler for it.
As indicated above there are flags you can set in Node so that it behaves differently when a promise rejects that has no handler for it. But it's better to avoid this situation than to fix it.
This situation would not occur if you would ensure that a promise has a rejection handler attached to it at the time of its creation. This is not the case in your current script.
Here is one way you could write your program so that your two promises get rejection handlers synchronously after their creation:
async function delay(ms) {
return new Promise(r=>setTimeout(r,ms))
}
async function fail(ms){
await delay(ms)
throw new Error("kek");
}
async function ok(ms){
await delay(ms)
return 1;
}
async function start() {
try{
let fail_p = fail(500);
let ok_p = ok(1000);
// Synchronously attach reject handlers to both promises:
const results = await Promise.all([ok_p, fail_p]);
console.log(...results); // This will not execute
}
catch(e){
// This executes before ok_p is fulfilled
console.log("ERR");
}
}
start().then(()=>console.log("Finish"));
Depending on your expectations, you may want to still await ok_p
, even when fail_p
has already rejected. If so, you could use Promise.allSettled
instead of Promise.all
and only raise an exception when both promises have settled (one fulfulled, one rejected). Note that you then have to throw the error explicitly, and so the use of try...catch
might become a less interesting option. It all depends on what you expect to happen in your real case...
Upvotes: 15