Trying Out the Promise.allSettled Combinator

Trying Out the Promise.allSettled Combinator

Have you ever come across a scenario where you have to make multiple requests and perform an action where all of these requests are fulfilled? Or perhaps you want to check how many requests are resolved and how many of them are rejected? In most cases, you would use Promise.all, a powerful combinator that is helpful when handling multiple requests with promises. But there are also some drawbacks to using this combinator.

Here is an example:

const urls = [ /* ... */ ];
const requests = urls.map(x => fetch(x)); // Imagine some of these will fail, and some will succeed.

// Short-circuits on first rejection, all other responses are lost
try {
  await Promise.all(requests);
  console.log('All requests have completed; now remove loading...');
} catch {
  console.log('At least one request has failed, but some of the requests still might not be finished! Oops.');
}

Let’s assume you have some API endpoints in the URL’s array and have sent requests to all of them. If you are using Promise.all and even a single request is rejected, then there will be a short circuit and the rest of the requests will be lost. You will also be unable to track the rest of the requests because the code block will fall into the catch case of the try-catch statement.

Due to these scenarios, Jason Williams (BBC), Robert Pamely (Bloomberg), and the champion Mathias Bynens (Google) proposed a new promise combinator known as Promise.allSettled.

What Exactly Is Promise.allSettled?

Promise.allSettled is a new combinator that will be added to JavaScript this year. Currently, the proposal is in a finished state.

Promise.allSettled() returns a promise that resolves after all of the given promises have either been resolved or rejected with an array of objects that describes the outcome of each promise.

Let's solve the previous example using the Promise.allSettled()

Promise.allSettled(requests).finally(() => {
  console.log('All requests are completed: either failed or succeeded, I don’t care');
  removeLoadingIndicator();
});

We have passed the requests array toPromise.allSettled and called finally(). This implementation is completely free from short circuits. The flow will never stop due to rejection. Therefore, there will be no need to write try-catch statements and extra checks. Looks cool, right? Why don’t we look at another use case then!

Another Use Case

What if we have to track which requests are fulfilled and which have been rejected? Traditionally, we would do it with the help of a helper function that takes a promise and returns its states to either fulfilled or rejected.

function reflect(promise) {
  return promise.then(
    (v) => {
      return { status: 'fulfilled', value: v };
    },
    (error) => {
      return { status: 'rejected', reason: error };
    }
  );
}

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

If you glance at the example above, you will see that we have created a helper function named reflect. This function takes a promise as an argument and calls it in the return statement. If the promise is successfully resolved, then it will fall into the first callback function of the .then() block, which is known as onFulfilled. Using this, we are returning the fulfilled value with the status of fulfilled from the onFulfilled callback function. If the promise is rejected, then it will fall into the second callback function, which is known as onRejected. Here, we are returning the error and the status that is rejected from this callback.

Next up, we are going to pass reflect as the callback function to promises.map(), which will then map over all the promises and return an array of all the results. To check for successful promises, we are filtering the results with the help of status. The successfulPromises array will contain all the fulfilled requests. And as you can see, this is a lot of work.

Let’s see how Promise.allSettled can simplify this process:

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

At first glance, you can feel the difference between the traditional approach vs. using Promise.allSettled. There is no need to write your own implementation. Promise.allSettled will map over the promises and return an array that will contain all the results — either fulfilled or rejected. We can filter the request just as we did in the traditional approach. All of this is clean, readable, and much more maintainable.

Bottom Line

You can feel the power of this amazing combinator in how it makes handling multiple requests easy and safe. If you want to use it right now, then this article offers a cool implementation. Until then, let’s just wait for this amazing feature to be added to the world of JavaScript!