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

Sentry blog image

How to reduce TTFB

In the past few years in the web dev world, we’ve seen a significant push towards rendering our websites on the server. Doing so is better for SEO and performs better on low-powered devices, but one thing we had to sacrifice is TTFB.

In this article, we’ll see how we can identify what makes our TTFB high so we can fix it.

Read more

Top comments (0)

The Most Contextual AI Development Assistant

Pieces.app image

Our centralized storage agent works on-device, unifying various developer tools to proactively capture and enrich useful materials, streamline collaboration, and solve complex problems through a contextual understanding of your unique workflow.

👥 Ideal for solo developers, teams, and cross-company projects

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