javascriptnode.jsasynchronousasync-awaitpromise

How to understand this unhandled promise rejection error?


According to the Description section of async function declaration in MDN:

async function foo() {
  const result1 = await new Promise((resolve) => setTimeout(() => resolve("1")));
  const result2 = await new Promise((resolve) => setTimeout(() => resolve("2")));
}
foo();

Note how the promise chain is not built-up in one go. Instead, the promise chain is constructed in stages as control is successively yielded from and returned to the async function. As a result, we must be mindful of error handling behavior when dealing with concurrent asynchronous operations.

For example, in the following code an unhandled promise rejection error will be thrown, even if a .catch handler has been configured further along the promise chain. This is because p2 will not be "wired into" the promise chain until control returns from p1.

async function foo() {
  const p1 = new Promise((resolve) => setTimeout(() => resolve("1"), 1000));
  const p2 = new Promise((_, reject) => setTimeout(() => reject("2"), 500));
  const results = [await p1, await p2]; // Do not do this! Use Promise.all or Promise.allSettled instead.
}
foo().catch(() => {}); // Attempt to swallow all errors...

I don’t understand why an unhandled promise rejection error will be thrown in the second foo function. As far as I understand, await p1 unwraps p1 so evaluates to '1', then await p2 unwraps p2 so throws '2', then foo catches '2', wraps '2' into a rejected promise with the value '2' and returns that promise, then catch(() => {}) is called on it so '2' should be caught. But this is not the case, instead an unhandled promise rejection error is thrown:

Promise {
  <pending>,
  [Symbol(async_id_symbol)]: 2132,
  [Symbol(trigger_async_id_symbol)]: 2126
}
> Uncaught '2'
> (node:32336) PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 21)

Solution

  • I had to play with it myself to understand it.

    The Problem is not awaiting a Promise and just letting the exception bubble up, that is fine. The actual Problem with this code is the timeout. Or rather, the specific times the Promises resolve.

    Notice that p2 rejects before p1 resolves:

    const p1 = new Promise((resolve) => setTimeout(() => resolve("1"), 1000));
    const p2 = new Promise((_, reject) => setTimeout(() => reject("2"), 500));
    

    and that we await p1 before we await p2:

    const results = [
                await p1,
                await p2
    ];
    

    This means, the moment p2 rejects we are not yet awaiting p2, so there is no then nor catch handler registered for p2 yet. Thats why its unhandled.