'Unexpected unhandledRejection event for promise which rejection does get handled

Updated, I've now tried explaining the behavior I'm seeing, but it'd still be great to have an answer from a credible source about the unhandledRejection behavor. I've also started a discussion thread on Reddit.


Why do I get an unhandledRejection event (for "error f1") in the following code? That's unexpected, because I handle both rejections in the finally section of main.

I'm seeing the same behavior in Node (v14.13.1) and Chrome (v86.0.4240.75):

window.addEventListener("unhandledrejection", event => {
  console.warn(`unhandledRejection: ${event.reason.message}`);
});

function delay(ms) {
  return new Promise(r => setTimeout(r, ms));
}

async function f1() {
  await delay(100);
  throw new Error("error f1");
}

async function f2() {
  await delay(200);
  throw new Error("error f2");
}

async function main() {
  // start all at once
  const [p1, p2] = [f1(), f2()];
  try {
    await p2;
    // do something after p2 is settled
    await p1;
    // do something after p1 is settled
  }
  finally {
    await p1.catch(e => console.warn(`caught on p1: ${e.message}`));
    await p2.catch(e => console.warn(`caught on p2: ${e.message}`));
  }
}

main().catch(e => console.warn(`caught on main: ${e.message}`));


Solution 1:[1]

You should be using try...catch to catch all the errors happening inside your try block:

try {
    await p2;
    // do something after p2 is settled
    await p1;
    // do something after p1 is settled
  }
catch(e) {
  // do something with errors e
}

EDIT:

window.addEventListener("unhandledrejection", event => {
  console.warn(`unhandledRejection: ${event.reason.message}`);
});

function delay(ms) {
  return new Promise(r => setTimeout(r, ms));
}

async function f1() {
  await delay(100);
  throw new Error("error f1");
}

async function main() {
  try {
  const p1 = await f1();
  await delay(200);
  }
  catch(e) {
    console.warn(`caught inside main: ${e.message}`);
  }
}

main().catch(e => console.warn(`caught on main: ${e.message}`));

Solution 2:[2]

I do not have a source but I think it works like this: The Promise.reject(new Error("Rejected!")); returns a rejected promise that will error next tick. so:

async function main3() {
    //this wil throw the error next tick
    const p1 = Promise.reject(new Error("Rejected!")); 
    //this will run immediately and attach the await to the promise (so it will not be rejected)
    await p1;
}

Then Promise.resolve will return its result to all .then handler next tick (we don't have them as the wont store the result anywhere) so:

async function main() {
    //this wil throw the error next tick
    const p1 = Promise.reject(new Error("Rejected!")); 
    //this will run immediately (and would give its value next tick)
    await Promise.resolve();
    //then this will run immediately and attach the await to the promise
    await p1;
}

Lastly a setTimeout with 0 delay does not immediately trigger, check: https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop and read the 0 delay section so:

async function main2() {
    //this wil throw the error next tick
    const p1 = Promise.reject(new Error("Rejected!"));  
    //setTimeout does with 0 does run not immediately.
    //because of this the await p1 does not get added before the promise is rejected
    await new Promise(r => setTimeout(r, 0));
    //so this does nothing and the prosime will reject
    await p1;
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 zhulien