DEV Community

Cover image for Race Conditions and Fallacies of `Promise.all`
Domi
Domi

Posted on • Edited on

3 2

Race Conditions and Fallacies of `Promise.all`

What fallacies does Promise.all have?

Promise.all is a convinient synchronization mechanism. However, unbeknownst to many, when facing rejections, Promise.all can cause two big headaches:

  1. Upon the first rejection, the promise created with Promise.all will settle with that first rejection. All its other unsettled promises are now "dangling". This means that code chained/nested in those promises is now running concurrently with all code that is chained after Promise.all. This can lead to ugly race conditions if you are not aware of and explicitely consider that possibility.
  2. If you have more than one rejection, any rejection that is not the first, not only is "dangling", but is explicitly muted. The JS engine will not report any of its unhandled rejections, that is not the first.

What about Promise.allSettled?

Promise.allSettled is not quite as user-friendly and even foregoes promise error/rejection handlers. You have to provide a fulfillment handler and manually loop over the results array to decipher whether you have any errors at all (i.e. Promise.allSettled(...).then(results => ...)). It's OK, if you make sure that you diligently handle all the information, but it makes things quite a bit more convoluted.

Solution

I present an alternative to Promise.all which uses Promise.allSettled and aggregates all errors.

NOTE: Just like, Promise.all, it ignores fulfilled values, in case of any rejection.

Pros:

  • Easy to use alternative to Promise.all that does not allow dangling, thereby preventing race conditions.
  • Reports all errors, not only the first

Cons:

  • Error aggregation mangles error objects into one big string. That can be further improved.
  • Promise chain not moving forward on first rejection can slow things down considerably in case the first error happens fast, but the critical path is slow.

The following code is also available in this gist:

/**
 * Fixes the "dangling problem" of Promise.all.
 *
 * {@link betterPromiseAll}
 * @see https://dev.to/domiii/a-solution-to-the-deep-flaws-of-promiseall-4aon-temp-slug-8454028
 */
async function promiseAll(promises) {
  const results = await Promise.allSettled(promises);
  const values = [];
  const errors = [];

  for (const result of results) {
    if (result.status === 'rejected') {
      errors.push(result.reason);
    }
    else {
      values.push(result.value);
    }
  }

  if (errors.length) {
    // NOTE: `AggregateError` seems not too mature YET. It's internal `errors` property is (currently, as of 2/2022) NOT rendered when reported, so we do some manual aggregation for now.
    // throw new AggregateError(errors, 'Promise.allSettled rejections');
    throw new Error(`${errors.length} promise rejections: ${errors.map((err, i) => `\n  [${i + 1}] ${err.stack || err}`).join('')}\n------`);
  }
  return values;
}

/** ###########################################################################
 * some samples
 * ##########################################################################*/
async function runSample(cb) {
  try {
    const result = await cb();
    console.log('########\nFULFILL:\n', result);
  }
  catch (err) {
    console.error('########\nREJECT:\n', err);
  }
}

// reject
runSample(() => {
  return betterPromiseAll([
    Promise.reject(1), 
    Promise.reject(new Error(2)), 
    Promise.resolve().then(() => { throw new Error(3); })
  ]);
});

// reject
runSample(() => {
  return betterPromiseAll([
    Promise.resolve(1),
    Promise.reject(new Error(2)), 
    Promise.resolve().then(() => { throw new Error(3); })
  ]);
});

// fulfill
runSample(() => {
  return betterPromiseAll([
    Promise.resolve(1),
    Promise.resolve(2),
    Promise.resolve(3)
  ]);
});
Enter fullscreen mode Exit fullscreen mode

Image of Timescale

Timescale – the developer's data platform for modern apps, built on PostgreSQL

Timescale Cloud is PostgreSQL optimized for speed, scale, and performance. Over 3 million IoT, AI, crypto, and dev tool apps are powered by Timescale. Try it free today! No credit card required.

Try free

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Discover a treasure trove of wisdom within this insightful piece, highly respected in the nurturing DEV Community enviroment. Developers, whether novice or expert, are encouraged to participate and add to our shared knowledge basin.

A simple "thank you" can illuminate someone's day. Express your appreciation in the comments section!

On DEV, sharing ideas smoothens our journey and strengthens our community ties. Learn something useful? Offering a quick thanks to the author is deeply appreciated.

Okay