.all()
, .race()
, .allSettled()
In this blog post, we take a look at three static methods of Promise
:
Promise.all()
and Promise.race()
which JavaScript has had since ECMAScript 6 when Promises were added to the language.Promise.allSettled()
which recently advanced to stage 4 and will therefore be part of ECMAScript 2020.Each of the following methods receives an iterable over input Promises and returns a single output Promise P
.
Promise.all<T>(promises: Iterable<Promise<T>>)
: Promise<Array<T>>
P
: if all input Promises are fulfilled.
P
[SC]: if one input Promise is rejected.
Promise.race<T>(promises: Iterable<Promise<T>>)
: Promise<T>
P
[SC]: if the first input Promise is settled.
Promise.allSettled<T>(promises: Iterable<Promise<T>>)
: Promise<Array<SettlementObject<T>>>
P
: if all input Promise are settled.
P
: never (*)Legend:
Given an asynchronous operation that returns a Promise. These are possible states of the Promise:
The combinator pattern is a pattern in functional programming for building structures. It is based on two kinds of functions:
When it comes to JavaScript Promises:
Promise.resolve()
, Promise.reject()
Promise.all()
, Promise.race()
, Promise.allSettled()
Next, we’ll take a closer look at Promise.all()
, Promise.race()
, and Promise.allSettled()
.
Promise.all()
This is the type signature of Promise.all()
:
Promise.all<T>(promises: Iterable<Promise<T>>): Promise<Array<T>>
Promise.all()
returns a Promise which is:
promises
are fulfilled.
promises
.This is a quick demo of the output Promise being fulfilled:
const promises = [
Promise.resolve('result a'),
Promise.resolve('result b'),
Promise.resolve('result c'),
];
Promise.all(promises)
.then((arr) => assert.deepEqual(
arr, ['result a', 'result b', 'result c']
));
The following example demonstrates what happens if at least one of the input Promises is rejected:
const promises = [
Promise.resolve('result a'),
Promise.resolve('result b'),
Promise.reject('ERROR'),
];
Promise.all(promises)
.catch((err) => assert.equal(
err, 'ERROR'
));
The following diagram illustrates how Promise.all()
works:
.map()
via Promise.all()
Array transformation methods such as .map()
, .filter()
, etc., are made for synchronous computations. For example:
function timesTwoSync(x) {
return 2 * x;
}
const arr = [1, 2, 3];
const result = arr.map(timesTwoSync);
assert.deepEqual(result, [2, 4, 6]);
What happens if the callback of .map()
is a Promise-based function (a function that maps normal values to Promises)? Then the result of .map()
is an Array of Promises. Alas, that is not data that normal code can work with. Thankfully, we can fix that via Promise.all()
: It converts an Array of Promises into a Promise that is fulfilled with an Array of normal values.
function timesTwoAsync(x) {
return new Promise(resolve => resolve(x * 2));
}
const arr = [1, 2, 3];
const promiseArr = arr.map(timesTwoAsync);
Promise.all(promiseArr)
.then(result => {
assert.deepEqual(result, [2, 4, 6]);
});
.map()
example Next, we’ll use .map()
and Promise.all()
to downlooad text files from the web. For that, we need the following tool function:
function downloadText(url) {
return fetch(url)
.then((response) => { // (A)
if (!response.ok) { // (B)
throw new Error(response.statusText);
}
return response.text(); // (C)
});
}
downloadText()
uses the Promise-based fetch API to download a text file as a string:
response
(line A).response.ok
(line B) checks if there were errors such as “file not found”..text()
(line C) to retrieve the content of the file as a string.In the following example, we download two text files:
const urls = [
'http://example.com/first.txt',
'http://example.com/second.txt',
];
const promises = urls.map(
url => downloadText(url));
Promise.all(promises)
.then(
(arr) => assert.deepEqual(
arr, ['First!', 'Second!']
));
Promise.all()
This is a simplified implementation of Promise.all()
(e.g., it performs no safety checks):
function all(iterable) {
return new Promise((resolve, reject) => {
let index = 0;
for (const promise of iterable) {
// Capture the current value of `index`
const currentIndex = index;
promise.then(
(value) => {
if (anErrorOccurred) return;
result[currentIndex] = value;
elementCount++;
if (elementCount === result.length) {
resolve(result);
}
},
(err) => {
if (anErrorOccurred) return;
anErrorOccurred = true;
reject(err);
});
index++;
}
if (index === 0) {
resolve([]);
return;
}
let elementCount = 0;
let anErrorOccurred = false;
const result = new Array(index);
});
}
Promise.race()
This is the type signature of Promise.race()
:
Promise.race<T>(promises: Iterable<Promise<T>>): Promise<T>
Promise.race()
returns a Promise q
which is settled as soon as the first Promise p
among promises
is settled. q
has the same settlement value as p
.
In the following demo, the settlement of the fulfilled Promise (line A) happens before the settlement of the rejected Promise (line B). Therefore, the result is also fulfilled (line C).
const promises = [
new Promise((resolve, reject) =>
setTimeout(() => resolve('result'), 100)), // (A)
new Promise((resolve, reject) =>
setTimeout(() => reject('ERROR'), 200)), // (B)
];
Promise.race(promises)
.then((result) => assert.equal( // (C)
result, 'result'));
In the next demo, the rejection happens first:
const promises = [
new Promise((resolve, reject) =>
setTimeout(() => resolve('result'), 200)),
new Promise((resolve, reject) =>
setTimeout(() => reject('ERROR'), 100)),
];
Promise.race(promises)
.then(
(result) => assert.fail(),
(err) => assert.equal(
err, 'ERROR'));
Note that the Promise returned by Promise.race()
is settled as soon as the first among its input Promises is settled. That means that the result of Promise.race([])
is never settled.
The following diagram illustrates how Promise.race()
works:
Promise.race()
to time out a Promise In this section, we are going to use Promise.race()
to time out Promises. The following helper function will be useful several times:
function resolveAfter(ms, value=undefined) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(value), ms);
});
}
resolveAfter()
returns a Promise that is resolved with value
after ms
milliseconds.
This function times out a Promise:
function timeout(timeoutInMs, promise) {
return Promise.race([
promise,
resolveAfter(timeoutInMs,
Promise.reject(new Error('Operation timed out'))),
]);
}
timeout()
returns a Promise whose settlement is the same as the one of whichever Promise settles first among the following two:
promise
timeoutInMs
millisecondsTo produce the second Promise, timeout()
uses the fact that resolving a pending Promise with a rejected Promise leads to the former being rejected.
Let’s see timeout()
in action. Here, the input Promise is fulfilled before the timeout. Therefore, the output Promise is fulfilled.
timeout(200, resolveAfter(100, 'Result!'))
.then(result => assert.equal(result, 'Result!'));
Here, the timeout happens before the input Promise is fulfilled. Therefore, the output Promise is rejected.
timeout(100, resolveAfter(2000, 'Result!'))
.catch(err => assert.deepEqual(err, new Error('Operation timed out')));
It is important to understand what “timing out a Promise” really means:
That is, timing out only prevents the input Promise from affecting the output (since a Promise can only be settled once). But it does not stop the asynchronous operation that produced the input Promise. That is a different subject matter.
Promise.race()
This is a simplified implementation of Promise.race()
(e.g., it performs no safety checks):
function race(iterable) {
return new Promise((resolve, reject) => {
for (const promise of iterable) {
promise.then(
(value) => {
if (settlementOccurred) return;
settlementOccurred = true;
resolve(value);
},
(err) => {
if (settlementOccurred) return;
settlementOccurred = true;
reject(err);
});
}
let settlementOccurred = false;
});
}
Promise.allSettled()
The feature “Promise.allSettled
” was proposed by Jason Williams, Robert Pamely, and Mathias Bynens.
This time, the type signatures are a little more complicated. Feel free to skip ahead to the first demo which should be easier to understand.
This is the type signature of Promise.allSettled()
:
Promise.allSettled<T>(promises: Iterable<Promise<T>>)
: Promise<Array<SettlementObject<T>>>
It returns a Promise for an Array whose elements have the following type signature:
type SettlementObject<T> = FulfillmentObject<T> | RejectionObject;
interface FulfillmentObject<T> {
status: 'fulfilled';
value: T;
}
interface RejectionObject {
status: 'rejected';
reason: unknown;
}
Promise.allSettled()
returns a Promise out
. Once all promises
are settled, out
is fulfilled with an Array. Each element e
of that Array corresponds to one Promise p
of promises
:
p
is fulfilled with the fulfillment value v
, then e
is{ status: 'fulfilled', value: v }
p
is rejected with the rejection value r
, then e
is{ status: 'rejected', reason: r }
Unless there is an error when iterating over promises
, the output Promise out
is never rejected.
The following diagram illustrates how Promise.allSettled()
works:
Promise.allSettled()
This is a quick first demo of how Promise.allSettled()
works:
Promise.allSettled([
Promise.resolve('a'),
Promise.reject('b'),
])
.then(arr => assert.deepEqual(arr, [
{ status: 'fulfilled', value: 'a' },
{ status: 'rejected', reason: 'b' },
]));
Promise.allSettled()
The next example is similar to the .map()
plus Promise.all()
example (from which we are borrowing the function downloadText()
): We are downloading multiple text files whose URLs are stored in an Array. However, this time, we don’t want to stop when there is an error, we want to keep going. Promise.allSettled()
allows us to do that:
const urls = [
'http://example.com/exists.txt',
'http://example.com/missing.txt',
];
const result = Promise.allSettled(
urls.map(u => downloadText(u)));
result.then(
arr => assert.deepEqual(
arr,
[
{
status: 'fulfilled',
value: 'Hello!',
},
{
status: 'rejected',
reason: new Error('Not Found'),
},
]
));
Promise.allSettled()
This is a simplified implementation of Promise.allSettled()
(e.g., it performs no safety checks):
function allSettled(iterable) {
return new Promise((resolve, reject) => {
function addElementToResult(i, elem) {
result[i] = elem;
elementCount++;
if (elementCount === result.length) {
resolve(result);
}
}
let index = 0;
for (const promise of iterable) {
// Capture the current value of `index`
const currentIndex = index;
promise.then(
(value) => addElementToResult(
currentIndex, {
status: 'fulfilled',
value
}),
(reason) => addElementToResult(
currentIndex, {
status: 'rejected',
reason
}));
index++;
}
if (index === 0) {
resolve([]);
return;
}
let elementCount = 0;
const result = new Array(index);
});
}
The npm package Promise.allSettled
contains the official polyfill.
All remaining sections are advanced.
For a Promise combinator, short-circuiting means that the output Promise is settled early – before all input Promises are settled. Two combinators short-circuit:
Promise.all()
: The output Promise is rejected as soon as one input Promise is rejected.Promise.race()
: The output Promise is settled as soon as one input Promise is settled.Once again, settling early does not mean that the operations behind the ignored Promises are stopped. It just means that their settlements are ignored.
Promise.all()
Consider the following code:
const asyncFunc1 = () => Promise.resolve('one');
const asyncFunc2 = () => Promise.resolve('two');
asyncFunc1()
.then(result1 => {
assert.equal(result1, 'one');
return asyncFunc2();
})
.then(result2 => {
assert.equal(result2, 'two');
});
Using .then()
in this manner executes Promise-based functions sequentially: only after the result of asyncFunc1()
is settled will asyncFunc2()
be executed.
Promise.all()
helps execute Promise-based functions more concurrently:
Promise.all([asyncFunc1(), asyncFunc2()])
.then(arr => {
assert.deepEqual(arr, ['one', 'two']);
});
Tip for determining how “concurrent” asynchronous code is: Focus on when asynchronous operations start, not on how their Promises are handled.
For example, each of the following functions executes asyncFunc1()
and asyncFunc2()
concurrently because they are started at nearly the same time.
function concurrentAll() {
return Promise.all([asyncFunc1(), asyncFunc2()]);
}
function concurrentThen() {
const p1 = asyncFunc1();
const p2 = asyncFunc2();
return p1.then(r1 => p2.then(r2 => [r1, r2]));
}
On the other hand, both of the following functions execute asyncFunc1()
and asyncFunc2()
sequentially: asyncFunc2()
is only invoked after the Promise of asyncFunc1()
is fulfilled.
function sequentialThen() {
return asyncFunc1()
.then(r1 => asyncFunc2()
.then(r2 => [r1, r2]));
}
function sequentialAll() {
const p1 = asyncFunc1();
const p2 = p1.then(() => asyncFunc2());
return Promise.all([p1, p2]);
}
Promise.all()
is fork-join Promise.all()
is loosely related to the concurrency pattern “fork join”. Let’s revisit an example that we have encountered previously:
Promise.all([
// (A) fork
downloadText('http://example.com/first.txt'),
downloadText('http://example.com/second.txt'),
])
// (B) join
.then(
(arr) => assert.deepEqual(
arr, ['First!', 'Second!']
));