On your way to become a "JavaScript Superstar" and "Promises whizz", you'll need to master the concept of handling multiple promises at the same time. There are some methods that are provided by the language to do just that, but before we dwell on those methods let's begin with understanding some basics of "states" and "fates" of promises.
Promises generally have 3 possible mutually exclusive states, they are:
promise.then(f)
will call f
as soon as possiblepromise.then(undefined, r)
will call r
"as soon as possible."An umbrella term, commonly used, for the above would be "settled" where a promise is said to be "settled" if it is not pending (that is if it is either fulfilled or rejected).
Promises have two possible mutually exclusive fates, they are:
Now that we have covered the concept of states and fates, we should now be able to understand promise concepts with common terminology. Let's move on to the gist of the article.
Promise.all
resolve all promises passed as an iterable object. The main characteristic is that it rejects completely when an input value is rejected. Basically, it follows an all-or-nothing methodology. Let us illustrate it with an example:
const promises = [
Promise.resolve('Luke'),
'Lea',
new Promise((resolve, reject) => setTimeout(resolve, 100, 'Han Solo')),
];
Promise.all(promises).then((characters) =>
console.log(`They are all getting along `, characters),
);
The output being will look something like:
We got a happy family [ 'Luke', 'Lea', 'Han Solo' ]
Now let us introduce a rejection to the array of promises
const promises = [
Promise.resolve('Luke'),
'Lea',
new Promise((resolve, reject) => setTimeout(resolve, 100, 'Han Solo')),
new Promise((_, reject) => setTimeout(reject, 100, 'Darth Vader')),
];
Promise.all(promises)
.then((characters) => console.log(`They are all getting along `, characters))
.catch((characters) =>
console.error(`They are getting along because of `, characters),
);
Now if you see the output should be:
> They are getting along because of Darth Vader
So in the first example, you can see that Promise.all
is fulfilled and all the names are returned. But in the next example, you can see that one rejected promise results in rejecting all the promises (all-or-nothing logic).
Promise.allSettled
method returns a promise that resolves after all of the given promises have either "fulfilled" or "rejected", with an array of objects that each describes the outcome of each promise. Let us illustrate it with an example:
const promises = [
Promise.resolve('Luke'),
'Lea',
new Promise((resolve, reject) => setTimeout(resolve, 100, 'Han Solo')),
];
Promise.allSettled(promises).then((characters) => console.log(characters));
The output will be different than before because Promise.allSettled
returns an array of objects that describes the output:
> [
{ status: 'fulfilled', value: 'Luke' },
{ status: 'fulfilled', value: 'Lea' },
{ status: 'fulfilled', value: 'Han Solo' }
]
Now let us introduce a rejection and analyze the output:
const promises = [
Promise.resolve('Luke'),
'Lea',
new Promise((resolve, reject) => setTimeout(resolve, 100, 'Han Solo')),
new Promise((_, reject) => setTimeout(reject, 100, 'Darth Vader')),
];
Promise.allSettled(promises).then((characters) => console.log(characters));
The output of the array of promises with rejection being:
> [
{ status: 'fulfilled', value: 'Luke' },
{ status: 'fulfilled', value: 'Lea' },
{ status: 'fulfilled', value: 'Han Solo' },
{ status: 'rejected', reason: 'Darth Vader' }
]
As you might have noticed Promise.allSettled
is a method that can be typically used when you have multiple asynchronous tasks that are not dependent on one another to complete successfully, unlike Promise.all()
may be more appropriate if the tasks are dependent on each other.
Promise.race()
method returns a promise that fulfills or rejects as soon as one of the promises in an iterable fulfills or rejects, with the value or reason from that promise. Hence, it short-circuits when the first input promise value is "settled". Let us illustrate this with a quick example:
const promises = [
Promise.resolve('Luke'),
'Lea',
new Promise((resolve, reject) => setTimeout(resolve, 100, 'Han Solo')),
];
Promise.race(promises).then((characters) => console.log(characters));
The output will be the first fulfilled promise status
> "Luke"
Now, lets analyze Promise.race
with a rejection:
const promises = [
new Promise((resolve, reject) => setTimeout(resolve, 100, 'Han Solo')),
new Promise((_, reject) => setTimeout(reject, 200, 'Darth Vader')),
];
Promise.race(promises)
.then((characters) => console.log(characters))
.catch((characters) => console.error(characters));
Since the rejection happens later, the output will be:
> "Han Solo"
Now, lets change things up and try to bring the rejection to be earlier
const promises = [
new Promise((resolve, reject) => setTimeout(resolve, 200, 'Han Solo')),
new Promise((_, reject) => setTimeout(reject, 100, 'Darth Vader')),
];
Promise.race(promises)
.then((characters) => console.log(characters))
.catch((characters) =>
console.error(`They are getting along because of `, characters),
);
The output will now be:
> They are getting along because of Darth Vader
Promise.any
takes an iterable of Promise objects and, as soon as one of the promises in the iterable fulfills, returns a single promise that resolves with the value from that promise. Each difference with respect to Promise.race
short-circuits on the first "fulfilled" promise. Let's illustrate with an example:
const promises = [
Promise.resolve('Luke'),
'Lea',
new Promise((resolve, reject) => setTimeout(resolve, 100, 'Han Solo')),
];
Promise.any(promises).then((characters) => console.log(`Statues `, characters));
Output will be the first promise that is fulfilled
> "Luke"
Now, let's try the same with a rejection that rejects the fastest
const promises = [
new Promise((resolve, reject) => setTimeout(resolve, 200, 'Han Solo')),
new Promise((_, reject) => setTimeout(reject, 100, 'Darth Vader')),
];
Promise.any(promises)
.then((characters) => console.log(characters))
.catch((characters) => console.error(characters));
The output here will be:
> "Han Solo"
Now, let's reject all promises
const promises = [
new Promise((_, reject) => setTimeout(reject, 200, 'Ben Solo')),
new Promise((_, reject) => setTimeout(reject, 100, 'Darth Vader')),
];
Promise.any(promises)
.then((characters) => console.log(characters))
.catch((characters) => console.error(characters));
The output for the above will be an AggregateError
> AggregateError: All promises were rejected
Essentially, as you can see this method is the opposite of Promise.all
.
An important note for Promise.any
would be to check the compatibility since it might not be completely available on all environments but still can be implemented with bluebird and polyfills.
AggregateError
is a new subclass ofError
that groups together individual errors
I hope we now have conquered the characteristics of the methods Promise.all
, Promise.allSettled
, Promise.race
and Promise.any
. We now are in a better state to write optimized Promises in JavaScript.
Happy Grizzly Coding 🐻