nodejs / help

:sparkles: Need help with Node.js? File an Issue here. :rocket:
1.45k stars 278 forks source link

Unhandled promise rejection when promise is not currently being await-ed on #4286

Closed ghost closed 8 months ago

ghost commented 8 months ago

Details

The following code will trigger an unhandled promise rejection.

Firefox has the same behavior.

Chrome however is interesting. At first Chrome console shows a red error but then Chrome changes its mind.

I'm just trying to understand why. I mean, logically it should work as the promise being rejected is eventually await-ed on later. But it seems like rejections must be await-ed on as they happen?

Node.js version

Not applicable.

Example code

(async () => {
    try {
        const promise = new Promise((resolve, reject) => {
            reject(new Error("oops"));
        });

        // Uncomment this to get a unhandled promise rejection.
        // await new Promise((resolve) => setTimeout(resolve, 1000)});

        await promise;
    } catch (reason) {
        console.log("caught the error");
    }
})();

Operating system

Not applicable.

Scope

Not applicable.

Module and version

Not applicable.

ljharb commented 8 months ago

In browsers, the console can be asynchronously updated; in node, it can not.

Promise unhandled rejection hooks were designed for (and really, only intended for use in) browsers, precisely because of this issue. Since node has chosen to implement these hooks, they will give false positives for a rejected promise that's handled later, and that's just unavoidable.

ghost commented 8 months ago

I come from a C++ background. In C++, promises are just containers whose state will be determined later. Nothing happens until the promise is observed.

In Javascript it appears that promises are implemented with a default catch that will throw an uncaught promise rejection.

While (some) browsers may decide to update the console (like you mentioned) when the promise rejection is eventually handled, an unhandled rejected exception was thrown (causing the error to be displayed, if only for a while) and any user-defined handler will have triggered.

This makes logically sound code as per the example break in unexpected ways. To solve this problem, I have to either write non-async/await code with .then() / .catch() chains which makes me sad. Or wrap the whole thing in Promise.all() and IIFE's which hurts readability.

I honestly don't know whether this is a language issue, specification issue or node issue. I've seen lots of code examples do the exact same thing so I'm clearly not the only one thinking that "I'll just await the promise later" will work. To me this very much looks like a footgun.

ljharb commented 8 months ago

It’s a node issue, and yes, it’s a footgun. You can disable or change the unhandled rejection behavior with a flag, or by adding your own unhandled rejection event listener.

All browsers behave the same here, as the HTML spec requires it; the language spec merely allows implementations to do it (largely because there’s no good way to allow what browsers do while prohibiting what node does).

ghost commented 8 months ago

Fair enough. Thanks for your comments.