What is Promise.all? How to implement it?
October 6, 2022
As a front-end engineer, you might use Promise.all() in daily development very often, but do you know how to implement this method? This is a very common question in interviews. If you still don’t know how to do it, let us explain it step by step through this article.
What is Promise.all()?
Before implementing this JavaScript built-in method, we need to know what it is doing. According to MDN's definition, Promise.all()
will
- Receives an
Iterable
(e.g. Array, Map, Set) with multiple promises inside it. - If the
Iterable
is empty, such as an empty Array, the fulfilled value will be an empty Array. - If
Iterable
is not empty and all promises are fulfilled, then return the fulfilled value in order; if one of the promises is rejected, it will reject immediately.
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, "foo");
});
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log(values);
});
// expected output: Array [3, 42, "foo"]
How to implement a simplified version of Promise.all()
Before implementing the detailed version of Promise.all()
, let's implement one that only handles Array as an example to understand how to implement the core concept, and then further discuss how to handle Iterable
. Let's look directly at the code first and see how much you can understand. Don’t worry if there is something you don’t understand, the following will explain it line by line through code comment:
function promiseAll(promises) {
if (!Array.isArray(promises)) {
return new TypeError("Arguments must be an array");
}
if (promises.length === 0) {
return Promise.resolve([]);
}
const outputs = [];
let resolveCounter = 0;
return new Promise((resolve, reject) => {
promises.forEach((promise, index) => {
promise()
.then((value) => {
outputs[index] = value;
resolveCounter += 1;
if (resolveCounter === promises.length) {
resolve(outputs);
}
})
.catch(reject);
});
});
}
Let's see how to do it through the following code comment:
function promiseAll(promises) {
// First check if the input is an array, if not, return an error
if (!Array.isArray(promises)) {
return new TypeError("Arguments must be an array");
}
// It is mentioned in the definition that if the input is empty (e.g. an empty array), resolve an empty array
if (promises.length === 0) {
return Promise.resolve([]);
}
return new Promise((resolve, reject) => {
// resolves with an array of resolved values when all the Promises in the array have resolved.
const resolvedValues = [];
// to do so, we need to keeps track of the number of resolved Promises
let resolvedCounter = 0;
// to get each promise's resolved value, we loops through each Promise in the promises array and calls .then method
promises.forEach((promise, index) => {
promise()
.then((value) => {
// stores the resolved value in the resolvedValues array
resolvedValues[index] = value;
// increment the resolvedCounter and check if it matches the length of the promises array
// if it does, resolves the returned Promise with the resolvedValues array
resolvedCounter++;
if (resolvedCounter === promises.length) {
resolve(resolvedValues);
}
})
.catch((error) => {
// if any of the Promises in the array is rejected, it rejects the returned Promise with the rejection reason.
reject(error);
});
});
});
}
Implement the full version of Promise.all()
The above version of Promise.all()
only handles the input of Array, but the actual Promise.all()
can receive all Iterable
, so we can further optimize the above version (note: if you can write the above version in the interview, you should be able to pass the interview. Of course, if you want to show you are detail-oriented in the interview, then further optimization is a better solution.
Think about it first, what can you do if you want to process any Iterable
? We can first determine whether the input input can be iterated, and if not, return an error early.
const isIterable =
((typeof promises === "object" && promises !== null) ||
typeof promises === "string") &&
typeof promises[Symbol.iterator] === "function";
if (!isIterable) {
return new TypeError("Arguments must be iterable");
}
Basically, the processing of the above steps is added, and the rest of the logic is similar to that of the Array version. The code is as follows (the differences will be annotated)
function promiseAll(promises) {
// Determine whether the input is Iterable
const isIterable =
((typeof promises === "object" && promises !== null) ||
typeof promises === "string") &&
typeof promises[Symbol.iterator] === "function";
// If not, return an error message
if (!isIterable) {
return new TypeError("Arguments must be iterable");
}
// Convert Iterable to Array, then the logic is the same as the one above
promises = Array.from(promises);
if (promises.length === 0) {
return Promise.resolve([]);
}
return new Promise((resolve, reject) => {
// a helper function that helps resolve the promise
function handleResolution(value, index) {
// stores the resolved value in the resolvedValues array
resolvedValues[index] = value;
// increment the resolvedCounter and check if it matches the length of the promises array
// if it does, resolves the returned Promise with the resolvedValues array
resolvedCounter++;
if (resolvedCounter === promises.length) {
resolve(resolvedValues);
}
}
// resolves with an array of resolved values when all the Promises in the array have resolved.
const resolvedValues = [];
// to do so, we need to keeps track of the number of resolved Promises
let resolvedCounter = 0;
// to get each promise's resolved value, we loops through each Promise in the promises array and calls .then method
promises.forEach((promise, index) => {
// check if it is thenable
if (promise.then) {
promise()
.then((value) => handleResolution(value, index))
.catch((e) => reject(e)); // reject immediately once there is an error
} else {
handleResolution(promise, index);
}
});
});
}