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

Viewed 397

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.

1 Answers

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]] »).

Related