loading...
Cover image for An in depth explanation of Promise.all and comparison with Promise.allSettled

An in depth explanation of Promise.all and comparison with Promise.allSettled

mpodlasin profile image Mateusz Podlasin ・10 min read

In this article, we will deal with 2 static methods available on a Promise constructor: all and allSettled.

We will find out what exactly they do, what are the differences between them and even how we could create our own polyfill for one of them.

This will give us a deep understanding of those methods and of how Promises work in JavaScript in general.

We will use simple examples, which you will be able to easily reproduce by yourself. So you are encouraged to follow along this article using some kind of online JavaScript repl, like this one.

Let's get started!

Promise.all

With the introduction of the native Promises in ES6, we also received a static Promise.all method.

It is one of the most basic ways to execute asynchronous tasks concurrently in JavaScript.

It's basic usage and behavior is very simple. You pass to it an array of Promises and then it waits for all of those Promises to get resolved. After that happens, you receive an array of results from all of the respective Promises.

Say we have two Promises and one async function. First Promise resolves to a number, second resolves to a string and the Promise which will be returned by the async function will resolve to a boolean:

const firstPromise = Promise.resolve(3);

const secondPromise = 
    new Promise(resolve => resolve('three'));

const createThirdPromise = async () => true;

We used an async function instead of a third Promise, to prove to you, that they are nothing more than functions returning Promises (you can read more on that in my article on async/await and Promise interoperability).

We also used two different ways to create a Promise that resolves immediately to a chosen value - using Promise.resolve method and simply using the Promise constructor.

The important thing to remember here is that there is a serious difference between the two first Promises and the async function. The two Promises already exist and are being executed. For example if they were representing HTTP requests, those requests would already be in execution at this moment in code.

Meanwhile, in the case of the async function, nothing gets executed yet - the Promise that will resolve to a boolean doesn't even exist yet! We will create it just before passing it to an array expected by the Promise.all.

We put emphasis on those points, because a common misconception is that Promise.all somehow begins the execution of Promises passed to it. But this is not the case. By the time the Promises are provided to Promise.all in an array, they are all already being executed. (You can read about Promises being an eager mechanism in my article on 3 biggest mistakes made when using Promises).

So Promise.all doesn't begin Promises execution, it just waits for them to finish. If all of the Promises already were resolved earlier (for example if all HTTP requests already have finished), then the Promise.all will resolve to a value almost immediately, because there is simply nothing to wait for anymore.

To drive this point home, take a look on how we call the Promise.all method:

Promise.all([
    firstPromise,
    secondPromise,
    createThirdPromise()
]);

First and second Promise already exist, so we simply pass them to the array. But because Promise.all expects Promises in that array, not functions (even if they are async!), we need to execute our async function, before passing its result to the array.

So you can see that by the time Promise.all will receive the Promises, they will all already be in execution. Perhaps some of them will even be already resolved!

Now we can use the array of results from all the Promises, which will be given to us when all of the Promises get resolved:

Promise.all([ /* nothing changes here */ ])
    .then(([a, b, c]) => console.log(a, b, c));

This code will result in 3 three true being printed to the console.

Note that we used an array destructuring to get three results from the three respective Promises. The order of the results in the array matches the order in which the Promises were passed to the Promise.all.

As you could probably figure out by now, Promise.all itself returns a Promise. Thanks to the async/await & Promise interoperability, we can retrieve the results in a nicer way, still utilizing handy array destructuring:

const [a, b, c] = await Promise.all([ /* nothing changes here */ ]);

console.log(a, b, c);

This looks much cleaner, but it will work only if that code is inside an async function or if your programming environment supports top-level await.

Promise.all error handling & Promise.allSettled

We covered the basic behavior of the Promise.all. Let's now look at how it behaves, when one of the Promises passed to it throws an error. This will help us understand why Promise.allSettled static method was introduced in 2019.

Let's modify our previous example, so that one of the Promises results in an error:

const firstPromise = Promise.resolve(3);

const secondPromise = Promise.reject('Some error message');

const createThirdPromise = async () => true;

You can see that now the second Promise will result in an error, because we used reject method instead of resolve.

Let's add an error handling function to our Promise.all usage example:

Promise.all([ /* nothing changes here */ ])
    .then(
        ([a, b, c]) => console.log(a, b, c),
        err => console.log(err)
    );

After we run this code, we only see Some error message logged to the console.

What happened? Well, because one of the Promises thrown an error, Promise.all simply rethrows that error as well, even if all of the other Promises actually resolved successfully.

Perhaps you already see an issue with that approach. Even though two out of the three Promises did not fail, we still can't use their results in any way, simply because one of the Promises have thrown an error.

That's because Promises always end in one of the two states - resolved or rejected (which is exactly the same thing as "thrown an error") - and there is no in-between.

The same applies to the Promise returned from the Promise.all method here - either all the Promises passed to the method successfully resolve and the output Promise gets resolved or (at least one) of the Promises rejects and our output Promise immediately rejects as well, not caring about the values from the other, perhaps successful, Promises.

So is there any way to regain those "missed" values from properly resolved Promises? Let's try to do just that.

What we can do is to try to handle the error from the Promise that we know will throw and return as it's new value the error object (in our case string) that has been thrown:

Promise.all([
    firstPromise,
    secondPromise.catch(error => error),
    createThirdPromise()
]);

Note how we used catch method and an arrow function to retrieve the thrown error object and immediately return it again, so that it becomes a new "successful" value of a Promise. This new Promise does not fail anymore - the error has been handled and this Promise resolves correctly to a value. So for the Promise.all method it is no longer a Promise that failed.

This way, even when the secondPromise throws an error, we will still receive values from the first and third Promises. What is more, instead of a value from the second Promise, we receive the error that it threw (a Some error message string), so we can handle an error based on that value.

But obviously in a real application we don't really know which Promises will fail, so we need to handle potential errors from all of them:

const promises = [
    firstPromise,
    secondPromise,
    createThirdPromise()
]

const mappedPromises = promises.map(
    promise => promise.catch(error => error)
);

Promise.all(mappedPromises)
    .then(([a, b, c]) => console.log(a, b, c));

Here we do the exact same thing as before, but we do it on all of the Promises, using map method. We then call Promise.all on mappedPromises, which have their errors handled, instead of original promises array, where the Promises can fail.

Now running this example ends in a 3 Some error message three logged to the console.

But the question appears. After that change, how can we know if the value that was printed to the console is a result of properly resolved Promise or a result of an error that we handled with catch? It turns out, we can't:

Promise.all(mappedPromises)
    .then(([a, b, c]) => {
        // Are a, b and c properly resolved values
        // or the errors that we caught?
    });

So in order to fix that, we need to complicate our code a little bit.

Instead of returning a values from the Promises directly, let's wrap each of them in an object that will have a special flag. That flag will tell us if the value comes from a resolved (or "fulfilled" as we also sometimes say) Promise or from a rejected one:

promise.then(
    value => ({ status: 'fulfilled', value }),
    reason => ({ status: 'rejected', reason })
)

You see that if this Promise resolves to a value, it will return an object with the flag fulfilled and the value itself under the property value.

If the Promise throws, it will return an object with the flag rejected and the error object itself under the property reason.

Note that this newly constructed Promise never throws an error, in other words it never gets into rejected state. It always resolves to a value, but this value is an object, informing us what really happened to the original Promise - whether it resolved or rejected.

Now we can apply this code to every Promise passed to the Promise.all:

const promises = [
    firstPromise,
    secondPromise,
    createThirdPromise()
]

const mappedPromises = promises.map(promise =>
    promise.then(
        value => ({ status: 'fulfilled', value }),
        reason => ({ status: 'rejected', reason })
    )
);

Promise.all(mappedPromises);

Let's now run that Promise.all function and log the results to the console:

Promise.all(mappedPromises)
    .then(([a, b, c]) => {
        console.log(a);
        console.log(b);
        console.log(c);
    });

After running the code you will see the following output:

{ status: 'fulfilled', value: 3 }
{ status: 'rejected', reason: 'Some error message' }
{ status: 'fulfilled', value: true }

That's exactly what we wanted!

Even if some of the Promises fail (like the second one did), we still get the values from the Promises that resolved correctly.

We are also getting error messages from the Promises that failed, so that we can handle those errors however necessary.

Furthermore we can easily tell which values come from fulfilled Promises and which ones come from rejected ones, by reading the status property.

Those three qualities are so often desired in programming with Promises, that Promise.allSettled was introduced.

It works exactly as our elaborate code above, but it does all that work for you.

You can see that, by adding following code to our snippet:

Promise.all(mappedPromises)
    .then(([a, b, c]) => {
        console.log(a);
        console.log(b);
        console.log(c);
        console.log('\n');
    })
    .then(() => Promise.allSettled(promises))
    .then(([a, b, c]) => {
        console.log(a);
        console.log(b);
        console.log(c);
    });

So we first run our Promise.all(mappedPromises) where we did error handling by hand. We log the results to the console and also log newline character \n to make a space in the console so that we can see the results from the two separate methods more clearly.

We than run Promise.allSettled(promises). Note that we run it on the original promises array, not mappedPromises. That's because allSettled will do all the error handling for us - that's the whole point of that method. So we simply pass it an array of our original Promises and we don't have to worry about anything else.

At the end we just log the results from Promise.allSettled, to compare them to the results from Promise.all.

Before running that code, make sure that you are in an environment that supports allSettled. After all, it is a fairly new addition. You can check the support here.

After running the code, you will see that - indeed - both methods behave in the same way and have exactly the same output:

// These are the results from Promise.all(mappedPromises)
{ status: 'fulfilled', value: 3 }
{ status: 'rejected', reason: 'Some error message' }
{ status: 'fulfilled', value: true }

// These are the results from Promise.allSettled(promises)
{ status: 'fulfilled', value: 3 }
{ status: 'rejected', reason: 'Some error message' }
{ status: 'fulfilled', value: true }

Note that we basically created a polyfill for Promise.allSettled. As an exercise you can try to wrap our code into a function allSettledPolyfill that behaves like allSettled and test it on some other examples.

Do they behave the same when used on more than 3 Promises? Do they behave the same when more Promises fail at the same time? Does passing an empty array to both of them end in the same result? Try it out for yourself!

Promise.all vs Promise.allSettled - summary

We explained in depth how Promise.all works. We then presented some of its characteristics that are sometimes undesirable. Those characteristics were a motivation to create a new method - Promise.allSettled, which we were able to program ourselves from scratch.

Let's finish the article by briefly summarizing the key differences between those two methods:

Promise.all accepts an array of Promises and returns a Promise resolving to an array of values from all the respective Promises. Promise.allSettled accepts the same input, but the array it resolves to, stores objects wrapping the returned values, not the values themselves.

If any of the Promises passed to Promise.all throws an error, Promise.all stops waiting for the other Promises and immediately rethrows the same error. Promise.allSettled on the other hand never throws an error. If some Promises fail, it still waits for all the other Promises to either resolve or reject and then simply marks the failed Promises with rejected flag on the object that it returns for that Promise.

And that's it! I hope that this article gave you a deep understanding of those two methods.

They both have their place and deciding which one to choose is always a matter of how do you want your failing Promises to be handled.

If you enjoyed this article, considered following me on Twitter, where I will be posting more articles on JavaScript programming.

Thank you for reading!

(Cover Photo by Jan Genge on Unsplash)

Posted on by:

mpodlasin profile

Mateusz Podlasin

@mpodlasin

I write in-depth articles about JavaScript, React and functional programming.

Discussion

markdown guide