Is it a documented behavior that Promise.all and Promise.race effectively make all promises "handled"?
Asked Answered
J

1

8

In the following, code unhandledRejection doesn't get fired for p2, even though it also gets rejected, albeit later than p1:

process.on('unhandledRejection', (reason, promise) => 
  console.log(`unhandledRejection: ${reason}`));

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

const p1 = async function f1(){
  await delay(100);
  throw new Error("f1");
}();

const p2 = async function f2(){
  await delay(200);
  throw new Error("f2");
}();

try {
  await Promise.race([p1, p2]);
  //await Promise.race([p1]);
}
catch (e) {
  console.error(e.message);
}

If I change the commented line like this:

  //await Promise.race([p1, p2]);
  await Promise.race([p1]);

... then unhandledRejection does get fired for p2, as expected. The same behavior is observed for Promise.all().

Thus, Promise.race and Promise.all effectively prevent the unhandledRejection event for promises which don't win the race but still get rejected. Is it a documented behavior? I can't seem to find any mentions of that in the specs.

Janitor answered 12/10, 2020 at 12:26 Comment(0)
A
8

Yes, Promise.race and Promise.all "handle" the result of all of the promises you pass into them, regardless of whether that result was relevant to the settlement of the promise from race/all. So the "losing" promise in Promise.race is still handled, even though the promise from Promise.race only reflects what happened with the winning promise. Similarly, if Promise.all rejects because one of its input promises rejects, any rejections from other input promises later are handled but nothing is done with them.

You can see this in the specification where it hooks up handlers to both fulfillment and rejection of each promise passed in, for instance in Step 3.i of PerformPromiseRace:

Perform ? Invoke(nextPromise, "then", « resultCapability.[[Resolve]], resultCapability.[[Reject]] »).

Analytic answered 12/10, 2020 at 12:30 Comment(2)
Nice! So I can rely upon this behavior in my code, I have a use case for that :) Thanks for the informative answer!Janitor
Following up, so as it turns, unhandledRejection will be fired if at the time of rejection the promise doesn't have any handlers attached. It doesn't matter if we attach the handler later. Interestingly, this is how it's different from a .NET Task, which can remain "dormant" and will only fire UnobservedTaskException if it's still unobserved when garbage-collected.Janitor

© 2022 - 2024 — McMap. All rights reserved.