Andrey
Andrey

Reputation: 121

Why does throwing an error in a not-yet-awaited async function call terminate node.js?

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

Answers (2)

Jim
Jim

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

trincot
trincot

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.

Why is there an unhandled rejection?

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.

Better practice

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

Related Questions