Wait for Multiple JavaScript Promises to Settle with Promise.allSettled()

Marius Schulz
InstructorMarius Schulz

Share this video with your friends

Send Tweet
Published 2 years ago
Updated a year ago

The Promise.allSettled() method accepts an array (or any other iterable) of promises as a parameter. It returns a Promise object that is fulfilled with an array of settlement objects. Each settlement object describes the settlement of the corresponding input promise.

Notice that the Promise object returned from the Promise.allSettled() method is only rejected if there's an error iterating over the input promises. In all other cases, it is fulfilled.

The Promise.allSettled() method is useful when you want to wait for multiple promises to settle, no matter whether they'll be fulfilled or rejected.

Marius Schulz: [0:00] Here we have the code from the previous lesson again. The only change I've made is that I've replaced the commas by new lines just so we can read our statistics a little more easily. If I go to the browser and refresh this page, we can see that, indeed, we are fetching the counts for films, planets, and species.

[0:17] Let's now make one of our requests fail and see what happens. I'm going to go over here and I'm going to access an endpoint that doesn't exist. Let's head back to the browser, open the Chrome DevTools, we'll go to the Network tab, and we're going to give this page a refresh. You can see that the request to the movies endpoint failed with a status code of 404 while the other two requests succeeded.

[0:43] Because the request to the movies endpoint failed, this first promise ends up being rejected. As a result of that, the promise returned from Promise.all ends up being rejected as well and we're not executing this fulfillment handler. Instead, we are executing the rejection handler. You can see the error message printed to the screen.

[1:04] However, wouldn't it be nice if we could display at least the data that we did manage to fetch successfully? In this case, we actually to load the data for planets and species, so why not display that? This is where the Promise.allSettled() method comes into play. This method has been added to JavaScript as part of ECMAScript 2020.

[1:25] First up, I'm going to take a look at Promise.all again. Promise.all accepts a bunch of promises. In this case, I'm going to pass a promise that has been resolved with a value of 42. I'm going to pass another promise that has been rejected with an error.

[1:43] We can see that the promise returned by Promise.all has been rejected. The returned promise is only fulfilled if all of these promises are fulfilled. If any of them is rejected, then the returned promise is rejected as well. Let's contrast this with Promise.allSettled now.

[2:04] Interesting. The promise returned by the allSettled method has been fulfilled, not rejected. Let's take a closer look.

[2:11] The Promise.allSettled method accepts a bunch of input promises and it returns another promise. Let's call that the settlement promise. The settlement promise is fulfilled once all of the input promises have been settled. That is, once they are no longer in the pending state.

[2:29] It doesn't matter if the input promises have been fulfilled or rejected, it just matters that they have been settled. The value of the settlement promise is an array of settlement objects, one for every input promise. In our case, it's an array of length 2, because we have passed two input promises.

[2:49] Each settlement object has a status property, and it describes how the corresponding promise has been settled. In our case, the first promise is fulfilled with a value of 42, so our settlement object has status "fulfilled" and a value of 42.

[3:05] Our second input promise is rejected with an error, and therefore, the corresponding settlement object has status "rejected" and it has the error as its rejection reason. Using the status property, we can tell apart which promises have been fulfilled versus which ones have been rejected.

[3:24] Notice that the promise returned by Promise.allSettled is always fulfilled. The only exception to that is if you pass a value to Promise.allSettled that cannot be iterated over. If you're not passing an array or some other iterable, then you'll get back a rejected promise. Otherwise, the promise will always be fulfilled.

[3:46] Let's now go back to our code and replace Promise.all by Promise.allSettled. Within our fulfillment handler, I'm going to add a bunch of console.log statements, so that we can see which values we have for films, planets, and species. All right, let's give this a refresh.

[4:07] In the browser console, we can see our three settlement objects. The first Promise was rejected, and therefore the first settlement object has a status of "rejected." The reason property contains the corresponding error. The other two settlement objects have a status of "fulfilled" and they've been fulfilled with our planets and with our species.

[4:32] You can see that the UI is currently clearly broken. We're seeing undefined films, undefined planets, and undefined species. This is because we're accessing the length property on films, planets, and species. However, the only properties available are status and reason, or status and value, depending on the settlement of the promise.

[4:53] Also, we only want to show the statistics for successful responses for promises that have been fulfilled. Let's go ahead and refactor our code.

[5:04] Initially, we don't know how many successful responses we're going to have. I'm going to collect our statistics in an array. At the end, I want to join together all of our statistics and write them to the innerText of our output div. We're going to take our statistics and join them with a new line.

[5:23] Now, we have to take a look at all three responses individually. If the status of the settlement is "fulfilled," then we want to add a statistic to our array. Notice that our films themselves are stored in the value property. This is why we're accessing films.value.length. We're going to do the same thing for planets and species. Let's see if this code is working.

[5:58] Our films couldn't be loaded because the movie's endpoint doesn't exist. However, we could load the data from the planets and species endpoints. We're displaying 60 planets and 37 species.

[6:09] Let's test the happy path. We'll make the films endpoint work. Indeed, now we're seeing three statistics and no more errors.

[6:20] We have built a UI that is resilient against occasional failures, so we could stop here. However, there's a few things I would like to clean up first. I don't really like the repetition that we have in our fulfillment handler. If we add another statistic in the future, that will lead to more copy and paste, so I want to get rid of that. Here's how we could do that.

[6:41] We can tuck on a fulfillment handler to every promise that we're passing to the allSettled method. In here, we can transform the result that we're getting back. In here, we could say {f.length} films. Let's do the same thing with planets and species as well. Let's rename this to p for planet and do that one more time. This time, with species.

[7:11] We can now build our statistics in a generic way. Instead of this destructuring pattern, we're going to use a single parameter called results. For our statistics, we're going to read the value of all fulfilled settlement objects. Let's make sure our code still works. Indeed, that's looking good. It's now really easy for us to add more statistics.

[7:39] Let's load the vehicles in the "Star Wars" universe as well. We're going to fetch them from the vehicles endpoint, rename this to v, and change this to vehicles. Another refresh and there we go, 39 vehicles.

[7:57] There is one last thing I want to tweak. What happens when all of the requests fail? I'm simulating that here, by accessing endpoints that don't exist. In that case, we're simply going to show an empty list of statistics. Instead of showing nothing, we should probably display an error message.

[8:17] Let's add a check for an empty statistics array. Let's see if that works. OK, cool. We're seeing the error message.

[8:32] If we restore all of these, reload one last time, and there we go, 6 films, 60 planets, 37 species, and 39 vehicles.

Yash Gupta
Yash Gupta
~ 2 years ago

Why does the Promise.allSettled() methods execute faster in this scenario even though I have disabled network caching?

The order seems to matter what will execute faster. If there is some caching going on where can I learn more about the internal caching mechanism? (Which is not happening due to network request being cached as I have already disabled it in my browser.)

const query = endpoint =>
  fetch(`https://jsonplaceholder.typicode.com/${endpoint}`).then(res =>
    res.json()
  );

Promise.all([query(`users`), query(`posts`)]).then(([users, posts]) => {
  console.time('Promise.all');
  console.log(`Promise.all`);
  console.log(users);
  console.log(posts);
  console.timeEnd('Promise.all');
});

Promise.allSettled([query(`users`), query(`posts`)]).then(([users, posts]) => {
  console.time('Promise.allSettled');
  console.log(`Promise.allSettled`);
  console.log(users.value);
  console.log(posts.value);
  console.timeEnd('Promise.allSettled');
});
Kunal Ranjan
Kunal Ranjan
~ 2 months ago

Hello schulz, why do .catch handler is unable to handle rejection in case all of the request is being failed.

Marius Schulz
Marius Schulzinstructor
~ 2 months ago

@Kunal: The promise returned by the Promise.allSettled() method is never rejected. As a result, you can't use the .catch() method to handle the case in which all promises were rejected. Instead, you should inspect the status property of the various fulfilment objects and check whether it's set to "rejected" for all promises.