Let me begin by saying that "Promise Collection Methods" is not something I've seen mentioned elsewhere but is my own way of referring to the various methods of the Promise API that work with multiple promises. They are:
Promise.all
Promise.allSettled
Promise.any
Promise.race
I've used Promise.all
many times in the past, and I was aware of the other methods but had not taken the time to actually build a demo of them. This weekend, I changed that. After spending a few hours in Sanctuary grinding my Necro character, I put down the controller and picked up the laptop. Here's what I built. As a note, everything shown here works in modern browsers, but you can check MDN for more information on compatibility if you need it.
Before getting into the important code, I built some methods to help me test things out, help me display stuff, and so forth. First, I knew I was going to build this on CodePen (I'll be embedding it below), and I wanted something would visually display in the browser, so I added this HTML:
<div id="log"><pre></pre></div>
And wrote this little utility:
const log = s => { document.querySelector('#log pre').innerHTML += s + '\n' };
This lets me then use log("like, whatever")
in my code and have it rendered out to the browser instead of the developer console.
Next, a function to handle returning promises with different times and optionally in an error state:
const makePromise = (name, secs, fail=false) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if(!fail) { log(`Good result for ${name}`); resolve(`Resolve from ${name}`); }
else { log(`Bad result for ${name}`); reject(`Fail from ${name}`); }
}, secs * 100);
});
}
Here are a few examples of it in use:
makePromise('Todd', 3);
makePromise('Scott', 1);
This will create two promises. One resolves in three seconds, the other in 1. Next:
makePromise('Ray', 2, true);
This will be rejected as an error in two seconds.
Also, note that I use the log
method here so I can see stuff resolving in real time. This will become important later.
Finally, I wrote a simple delay function:
const delay = x => {
return new Promise(resolve => setTimeout(resolve, x*1000));
}
This will let me stuff like:
delay(4);
As a quick and hacky way to delay a few seconds. In theory, I could have made makePromise
make use of it, but I didn't bother updating it. Alright, let's get started.
The all
method has the following behavior:
Given an array of Promises, resolve when all are done, or immediately when any throw an error.
It resolves with an array of results that match the results of inputs. Here's my first test:
log('TEST ONE - Promise.all(all good)');
let test = [
makePromise('alpha', 3),
makePromise('bob', 1),
makePromise('charly', 3)
];
results = await Promise.all(test);
log('Results from test with Promise.all');
log(JSON.stringify(results));
Looking at the code, you can see that bob
will return before alpha
and charly
. Here's the output:
Good result for bob
Good result for alpha
Good result for charly
Results from test with Promise.all
["Resolve from alpha","Resolve from bob","Resolve from charly"]
Notice that even though the order was different than the input, the results match the input, which is great.
Now, let's throw an error into the mix:
log('\n\nTEST TWO - Promise.all (one bad)');
test = [
makePromise('alpha', 3),
makePromise('failwhale', 1, true),
makePromise('charly', 3)
];
try {
let results = await Promise.all(test);
} catch(e) {
log(`Expected failure in test: ${e}`);
}
I wrapped the call in a try/catch
to handle the rejection. Here's the output:
TEST TWO - Promise.all (one bad)
Bad result for failwhale
Expected failure in test: Fail from failwhale
Good result for alpha
Good result for charly
Notice that my handler fires as soon as the error occurs, but the other promises are still running. You don't have to use try/catch
. Since the result of Promise.all
is itself a promise, you can use the catch
method of Promise instead:
log('\n\nTEST TWO A - Promise.all (one bad), modified handler.');
Promise.all(test).then(results => log('all good')).catch(e => log('one bad'));
This returns, as you would expect, just one bad
.
While Promise.all
is good for when you're pretty sure everything is going to work out ok (and remember, a Fetch call to an API may run just fine, but the API itself may return with an error), you may find [allSettled
] method a bit more flexible. It's behavior is:
Given an array of Promises, resolve when all are done and report on the success or failure of each Promise.
This means you can now rely on knowing when everything is done and handle the success or failure of each one by one. Here's an example:
log('\n\nTEST THREE - Promise.allSettled (all good)');
test = [
makePromise('alpha', 3),
makePromise('bob', 1),
makePromise('charly', 3)
];
results = await Promise.allSettled(test);
log('Results from test with Promise.allSettled');
log(JSON.stringify(results,null,'\t'));
The result now is a bit different:
[
{
"status": "fulfilled",
"value": "Resolve from alpha"
},
{
"status": "fulfilled",
"value": "Resolve from bob"
},
{
"status": "fulfilled",
"value": "Resolve from charly"
}
]
Now we get both the value from the resolve
as well as a status
flag. Here's an example of a failure. First, the calls:
log('\n\nTEST FOUR - Promise.allSettled (one bad)');
test = [
makePromise('alpha', 3),
makePromise('failwhale', 1, true),
makePromise('charly', 3)
];
results = await Promise.allSettled(test);
log('Results from test with Promise.allSettled');
log(JSON.stringify(results,null,'\t'));
And then the result:
[
{
"status": "fulfilled",
"value": "Resolve from alpha"
},
{
"status": "rejected",
"reason": "Fail from failwhale"
},
{
"status": "fulfilled",
"value": "Resolve from charly"
}
]
You can see the failure in the second result. Woot. I think this is my favorite so far. Going on...
The any
method works like so:
Given an array of Promises, resolve when any of the Promises resolves, or reject if all of them fail.
This one's kind of interesting. It's basically the "try really hard for something to work" collection method. Here's a first example:
log('\n\nTEST FIVE - Promise.any(all good)');
test = [
makePromise('alpha', 3),
makePromise('bob', 1),
makePromise('charly', 3)
];
results = await Promise.any(test);
log('Results from test with Promise.any');
log(results);
In this one, bob
is the winner:
TEST FIVE - Promise.any(all good)
Good result for bob
Results from test with Promise.any
Resolve from bob
Good result for alpha
Good result for charly
Next, here's one with an error. It's the quickest, but any
keeps trying:
log('\n\nTEST SIX - Promise.any(one bad)');
test = [
makePromise('alpha', 3),
makePromise('bad bob', 1, true),
makePromise('charly', 3)
];
results = await Promise.any(test);
log('Results from test with Promise.any');
log(results);
And the output:
TEST SIX - Promise.any(one bad)
Bad result for bad bob
Good result for alpha
Results from test with Promise.any
Resolve from alpha
Good result for charly
Finally, here's one where they all fail.
log('\n\nTEST SEVEN - Promise.any(all bad)');
test = [
makePromise('alpha', 3, true),
makePromise('bad bob', 1, true),
makePromise('charly', 3, true)
];
try {
let results = await Promise.any(test);
log(results);
} catch(e) {
log(`Expected failure in test: ${e}`);
log(e.errors);
}
Notice I log e.errors
- this is an additional value thrown by the method that contains an array of all the messages from the failed promises. (It's an AggregateError).
Here's the output:
TEST SEVEN - Promise.any(all bad)
Bad result for bad bob
Bad result for alpha
Bad result for charly
Expected failure in test: AggregateError: All promises were rejected
Fail from alpha,Fail from bad bob,Fail from charly
For the final method, I'll cover, race
has this behavior:
Given an array of promises, resolve or reject with whatever happens first.
Here are a few examples. First, all good:
log('\n\nTEST RAY EIGHT - Promise.race(all good)');
test = [
makePromise('alpha', 3),
makePromise('bob', 1),
makePromise('charly', 3)
];
results = await Promise.race(test);
log('Results from test with Promise.race');
log(results);
And the output:
TEST RAY EIGHT - Promise.race(all good)
Good result for bob
Results from test with Promise.race
Resolve from bob
Good result for alpha
Good result for charly
And here's one with a 'bad' winner:
log('\n\nTEST NINE - Promise.race(bad guy wins)');
test = [
makePromise('alpha', 3),
makePromise('worst bob', 1, true),
makePromise('charly', 3)
];
try {
let results = await Promise.race(test);
} catch(e) {
log(`Expected failure in test: ${e}`);
}
And its results:
TEST NINE - Promise.race(bad guy wins)
Bad result for worst bob
Expected failure in test: Fail from worst bob
Good result for alpha
Good result for charly
Pretty simple to understand, I think. The MDN docs have a great example of how to use Promise.race
to add timeouts to network calls.
You can see all of the above yourself at the following CodePen, but you may want to open it up in a new tab.
Also published here.