loading...
Cover image for Solve* all your problems with Promise.allSettled()

Solve* all your problems with Promise.allSettled()

irreverentmike profile image Mike Bifulco Originally published at mike.biful.co on ・4 min read

(Note: This post was inspired by a talk from Wes Bos at JAMstack_conf_nyc. Thanks for the tip, Wes!)

Of late, I've found myself building JavaScript web applications with increasing complexity. If you're familiar with modern JavaScript, you've undoubtedly come across Promise - a construct which helps you execute code asynchronously. A Promise is just what it sounds like: you use them to execute code which will (promise to) return a value at some point in the future:

Check out this somewhat-contrived example, wherein we asynchronously load comments on a blog post:

const loadComments = new Promise((resolve, reject) => {
  // run an asynchronous API call
  BlogEngine.loadCommentsForPost({ id: '12345' })
    .then(comments => {
      // Everything worked! Return this promise with the comments we got back.
      resolve(comments)
    })
    .error(err => {
      // something went wrong - send the error back
      reject(new Error(err))
    })
})

There's also an alternative syntax pattern, async / await, which lets you write promises in a more legible, pseudo-serial form:

const loadComments = async () => {
  try {
    const comments = await BlogEngine.loadCommentsForPost({ id: '12345' })
    return comments
  } catch (err) {
    return new Error(err)
  }
}

Dealing with multiple promises

Inevitably, you'll find yourself in situations where you need to execute multiple promises. Let's start off simply:


const postIds = ['1', '2', '3', '4', '5'];
postIds.forEach(async (id) => {
  // load the comments for this post
  const comments = await loadComments(id);

  // then do something with them, like spit them out to the console, for example
  console.log(`Returned ${comments.length} comments, bru`);
})

Easy! A quick loop gets us comments for every post we're interested in. There's a catch here, though - the await keyword will stop execution of the loop until loadComments returns for each post. This means we're loading comments for each post sequentially, and not taking advantage of the browser's ability to send off multiple API requests at a time.

The easiest way to send off multiple requests at once is with Promise.all(). It's a function which takes an array of Promises, and returns an array with the responses from each promise:

const postIds = ['1', '2', '3', '4', '5'];
const promises = postIds.map(async (id) => {
  return await loadComments(id);
};

const postComments = Promise.all(promises);

// postComments will be an Array of results fromj the promises we created:
console.log(JSON.postComments);
/*
[
  { post1Comments },
  { post2Comments },
  etc...
]
*/

There is one important catch (lol) with Promise.all(). If any of the promises sent to Promise.all() fails or rejects, everything fails. From the MDN Web Docs (emphasis mine):

The Promise.all() method returns a single Promise that resolves when all of the promises passed as an iterable have resolved or when the iterable contains no promises. It rejects with the reason of the first promise that rejects.

Well damn, it turns out that Promise.all() is fairly conservative in its execution strategy. If you're unaware of this, it can be pretty dangerous. In the example above, it's not great if loading comments for one post causes the comments for every post not to load, right? Damn.

Enter Promise.allSettled()

Until fairly recently, there wasn't a spectacular answer for scenarios like this. However, we will soon have widespread access to Promise.allSettled(), which is currently a Stage 3 proposal in front of the ECMAscript Technical Committee 39, the body in charge of approving and ratifying changes to ECMAscript (aka "JavaScript", for the un-initiated).

You see, Promise.allSettled() does exactly what we'd like in the example above loading blog comments. Rather than failing if any of the proments handed to it fail, it waits until they all finish executing (until they all "settle", in other words), and returns an array from each:

(this code sample is cribbed from the github proposal - go give it a look for more detail)

const promises = [fetch('index.html'), fetch('https://does-not-exist/')]
const results = await Promise.allSettled(promises)
const successfulPromises = results.filter(p => p.status === 'fulfilled')

That's it! Super easy to use.

Using Promise.All() now (updated!)

Update 4/26/19
Install the core-js package and include this somewhere in your codebase:

import 'core-js/proposals/promise-all-settled'

Original post:
Ok, here's the thing - that's the tricky part. I wrote this post thinking it'd be as easy as telling you to use a stage-3 preset in the .babelrc config on your project. As it turns out, as of v7, Babel has stopped publishing stage presets! If that means anything to you, you ought to read their post.

The answer right now is that it's not yet a great idea to use Promise.allSettled(), because it isn't widely supported. To boot, as far as I can tell, there's not a babel config extension which will add support to your projects. At the moment, the best you'll get is a polyfill or an alternative library which implements allSettled().

I know that can be disappointing - be sure that I've got a dozen problems that would be well-served with this new bit of syntax. What I want you to focus on, though, is how amazing it is that JavaScript is continuing to grow. It's exciting and really freaking cool to see that these additions to the language are also being worked on in public. Open Source is such a beautiful thing!

If you're really motivated to use Promise.All() in your code, you'd do well to contribute to the process in some way. This may be something as small as writing your own polyfill, or giving feedback to the folks involved with tc39, or one of the alternative libraries to use.

Footnote

I'll do my best to keep this post up to date. When allSettled is released, I'll let y'all know. 👍

(Cover photo for this post is by Valentin Antonucci on Unsplash. Thank you for your work!)

Discussion

pic
Editor guide
Collapse
anduser96 profile image
Andrei Gatej

As far as I know, there is a way to get a promise resolved from Promise.all(), event if at least one is rejected.

// Handling multiple promises even if some fail

function dummyPromise(param) {
    return param !== 5 ? Promise.resolve(param) : Promise.reject('no!!!')
}

const requests = [{
        trello: dummyPromise(1),
        medium: dummyPromise(2)
    },
    {
        trello: dummyPromise(3),
        medium: dummyPromise(4)
    },
    {
        trello: dummyPromise(5).catch(() => 'no!'),
        medium: dummyPromise(6)
    },
]

const promises = []

requests.forEach(req => promises.push(...Object.values(req)))

Promise.all(promises)
    .then(console.log)
    .catch(console.error)
// => [ 1, 2, 3, 4, 'no!', 6 ]​​​​​

So the idea is to add a catch() to each promise inside Promise.all().
Hope this helps.

Collapse
brokenseal profile image
Davide Callegari

There are a couple of issues with your example:

  1. async is missing
  2. .each on arrays in js does not exist
  3. iteration on arrays on a forEach loop does not result in synchronous running of promises, rather a for-loop with async/await would result in the problem you explained.

One idea I had some time ago was to use this method in order to obtain something similar to what the allSettled method will do:

Promise.all([
promise1.catch(e => e),
promise2.catch(e => e)
]).then([ resultOrError, resultOrError ], ()=> ...)

Collapse
irreverentmike profile image
Mike Bifulco Author

Ah man, great points, all three! I went and edited to address (1) and (2). Depending on the example you're talking about with (3) above, that was more or less my point - running a bunch of awaited calls in a loop is sequential for better or worse.

Thanks for reading, I appreciate your insight!

Collapse
brokenseal profile image
Davide Callegari

Hello again,
nice article, really :)

Sorry about the previous comment but I was replying from my mobile phone.

My point is that I would rewrite your third example like this:

const postIds = ['1', '2', '3', '4', '5'];

async function loadCommentsSequentially(ids) {
  for (let id of postIds) {
    // wait for comments to be loaded and then load the comments from the next post
    return await loadComments(id);
  }
}

loadPostsSequentially().then((comments) => ...)

so that your next paragraph would make more sense:

There's a catch here, though - the await keyword will stop execution of the loop until loadComments returns for each post.

because in your example, using the forEach loop, the execution is not sequential, but rather a list of promises are created and are "lost" (nobody waits for them to be completed).

I hope I explained it better this time :)

Thread Thread
irreverentmike profile image
Mike Bifulco Author

Ahhhh yeah, I see I see - yep, that makes sense! Thanks again Davide!

Collapse
johncarroll profile image