Use Promise.all to Wait for Multiple Promises in Node.js
Sometimes in Node, we need to execute multiple asynchronous operations concurrently, wait for them all to complete, and then do something with the combined result. An example of this would be to wait for multiple API calls to finish before gathering results and using them to form another API call. To accomplish this, we can use JavaScript’s Promise.all()
to wait for multiple Promises to resolve, before doing something with their results. This allows us to write code in Node that’s easier to maintain and understand.
In this tutorial we’ll:
- Learn how to use JavaScript’s
Promise.all
to wait for multiple Promises to return - Practice with a use case for
Promise.all
using NPMnode-fetch
By the end of this tutorial you should be able to use Promise.all
to simultaneously execute multiple asynchronous operations.
Goal
Use Promise.all
to make multiple concurrent fetch
requests and then act on the data only once they have all resolved.
Prerequisites
- Understanding Promises in Node.js
- Array.map (developer.mozilla.org)
Watch
What is Promise.all
?
Promise.all
is a utility that takes an iterable (most commonly an array) of Promises as input, and returns a single Promise that resolves once all the Promises have resolved, and rejects with the error from the first Promise that rejects.
Using Promise.all
, we can make multiple API calls, create a Promise that resolves when all the calls have finished, and return the results in an array. This lets us create a “basket” of Promises, and handle their return values all at once.
If any of the Promises passed to Promise.all
reject, Promise.all
rejects with the value of the Promise that rejected, whether or not the other Promises have resolved. This is considered “fail fast” behavior; as soon as one Promise rejects, the Promise returned by Promise.all
will immediately reject as well.
How to use Promise.all
First we need an array of Promises to pass to Promise.all
. Next, we can add a then
handler to the returned Promise, which will receive the result of the resolved Promises. To be safe, we attach a catch
handler so any potential errors can be handled.
Here, we create a single new Promise that resolves after all of these timeouts wrapped in Promises complete:
const all = Promise.all([ new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)), new Promise((resolve, reject) => setTimeout(() => resolve(2), 2000)), new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))]).catch(err => console.log("Promise was rejected!", err));
all.then(results => console.log(results)); // [1, 2, 3]
That new Promise, called all
above, is like a “basket”, into which we have stuffed an array of other Promises. It will wait until all the Promises have been resolved and bundle their resolved results together into an array which is passed to the then
handler. The resolved values in the results array will be in the same order as the Promises were.
Note that all 3 inner Promises are started at the same time. Instead of taking 6 total seconds, our new Promise should resolve after about 3 seconds, since it will resolve once all timers have completed, and the longest timeout is 3 seconds.
Promise.all
is most useful when you have multiple Promises, and want to know when they have all completed their operations before doing something with their results.
Example with fetch
Here is a more real-world use case, requesting info from an API based on an array of ids. To run this example yourself, you need to install node-fetch
from npm.
const fetch = require("node-fetch");const url = "https://jsonplaceholder.typicode.com/todos";const ids = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// fetch returns a Promise.const getTodo = id => fetch(`${url}/${id}`) .then(response => response.json());
// Map over our ids, returning a promise for each one.const arrayOfPromises = ids.map(id => getTodo(id));
Promise.all(arrayOfPromises) .then(todos => todos.map(todo => todo.title)) .then(titles => console.log(titles)) // logs titles from all the todos .catch(err => console.log(err));
The function getTodo
takes an id
and returns a Promise. We map over the array of ids to create a new array of Promises, which we then pass to Promise.all
. That executes each Promise and waits for them all to resolve.
If one of the API calls throws an error or the Promise otherwise rejects, Promise.all
is rejected immediately and .catch
is called instead of .then
.
This is much cleaner than nesting the calls one after another in callbacks, or chaining them together with .then
handlers. Additionally, with Promise.all
your Promises will run concurrently, not sequentially like they would if chained.
Promise.all
saves us from writing error-prone and messy code with either callback nesting or unnecessary Promise chains by allowing us to compose a new Promise.
Fault-tolerant Promise.all
Let’s say you’re using Promise.all
on 10 API calls, and 1 of them throws an error. Normally, the other Promises will be halted, and you won’t get the results from any of them. To still receive results from Promise.all
in a case where some Promises reject, we need to make Promise.all
fault tolerant.
To avoid losing the other request responses, we can attach catch handlers to the individual Promises passed to Promise.all
. This way we are catching the errors they might throw, instead of letting them bubble up to the Promise.all
which will cause the Promise to reject.
const promises = [ fetch(url) fetch(url) fetch(url) Promise.reject(new Error('This fails!')) ]
Promise.all(promises.map(promise => promise.catch(error => error))) .then(results => { // we get results even if a promise rejected! // results can be a mix of errors and success values; you'd have to sort them yourself})
This is a handy trick if you need to access the data from the requests regardless of whether any fail.
Recap
When you need to make several asynchronous calls and want to gather their results together, use Promise.all
. Promise.all
lets us say, “Here’s a collection of Promises, let me know when they’ve all completed, or as soon as one goes wrong, and I’ll handle it from there.” This allows us to write code that’s easier to maintain and understand.
Further your understanding
- Give some examples of when
Promise.all
might help speed up your application. - Can you think of any way to salvage the results when 1 of the 10 API calls fails?
Additional resources
- MDN Promise.all docs (developer.mozilla.org)