JavaScript Series #69: Mastering Concurrency with Promise.all() and Promise.race()
As JavaScript developers, we constantly deal with asynchronous operations: fetching data from APIs, reading files, handling user input, and more. Promises provide a robust way to manage these operations, making our code more readable and maintainable than traditional callbacks. But what happens when you need to coordinate multiple asynchronous tasks?
That's where the powerful static methods Promise.all() and Promise.race() come into play. These methods allow us to orchestrate several promises, giving us fine-grained control over how we handle their collective resolution or rejection. Let's dive deep into each.
Orchestrating Multiple Promises with Promise.all()
Promise.all() is your go-to method when you have multiple independent promises that all need to complete successfully before you proceed. Think of it as a "wait for all" mechanism.
How it Works
- It takes an iterable (e.g., an array) of promises as input.
- It returns a single new Promise.
- This new promise resolves when all of the input promises have resolved successfully. The resolution value is an array containing the resolved values of the input promises, in the same order as they were passed.
- This new promise rejects as soon as any one of the input promises rejects. The rejection reason is the reason of the first promise that rejected. This is often referred to as "fail-fast" behavior.
Promise.all() Example: All Successful
Let's simulate fetching data from two different API endpoints concurrently.
const fetchDataFromAPI1 = new Promise((resolve) => {
setTimeout(() => resolve('Data from API 1'), 1000); // Resolves after 1 second
});
const fetchDataFromAPI2 = new Promise((resolve) => {
setTimeout(() => resolve('Data from API 2'), 500); // Resolves after 0.5 seconds
});
console.log('Fetching data...');
Promise.all([fetchDataFromAPI1, fetchDataFromAPI2])
.then((results) => {
// 'results' will be an array: ['Data from API 1', 'Data from API 2']
console.log('All data fetched successfully:', results);
})
.catch((error) => {
console.error('One of the fetches failed:', error);
});
// Expected Output after 1 second:
// Fetching data...
// All data fetched successfully: [ 'Data from API 1', 'Data from API 2' ]
Notice that even though fetchDataFromAPI2 resolves faster, Promise.all() waits for the slowest promise (fetchDataFromAPI1) to complete before resolving, and the results are still in the original order.
Promise.all() Example: Failing Fast
Now, let's see what happens if one of the promises rejects.
const fetchUser = new Promise((resolve) => {
setTimeout(() => resolve({ id: 1, name: 'Alice' }), 800);
});
const fetchPermissions = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('Permission denied!')), 300); // Rejects first
});
const fetchSettings = new Promise((resolve) => {
setTimeout(() => resolve({ theme: 'dark' }), 600);
});
console.log('Attempting to fetch user data, permissions, and settings...');
Promise.all([fetchUser, fetchPermissions, fetchSettings])
.then((results) => {
console.log('All operations successful:', results);
})
.catch((error) => {
console.error('An operation failed:', error.message);
});
// Expected Output after 0.3 seconds:
// Attempting to fetch user data, permissions, and settings...
// An operation failed: Permission denied!
In this scenario, Promise.all() rejects immediately with the error from fetchPermissions, even before fetchUser or fetchSettings would have resolved.
When to Use Promise.all()
- When you need to load multiple critical resources (e.g., user profile, dashboard data, configuration files) simultaneously, and you can't render the UI until all are available.
- When performing parallel calculations that all contribute to a final result.
- Batch processing of multiple API requests where all responses are required.
The Race to Resolution: Introducing Promise.race()
Promise.race() is for scenarios where you only care about the very first promise to "settle" (either resolve or reject). As its name suggests, it's a race, and the first one to finish wins.
How it Works
- It also takes an iterable of promises as input.
- It returns a single new Promise.
- This new promise settles (resolves or rejects) with the same value or reason as the first input promise that settles.
- If the first promise resolves,
Promise.race()resolves with that promise's value. - If the first promise rejects,
Promise.race()rejects with that promise's reason.
Promise.race() Example: First to Resolve Wins
Imagine fetching data from multiple redundant servers and wanting to use the response from the fastest one.
const fetchFromServerA = new Promise((resolve) => {
setTimeout(() => resolve('Data from Server A'), 800);
});
const fetchFromServerB = new Promise((resolve) => {
setTimeout(() => resolve('Data from Server B'), 300); // This one will win
});
const fetchFromServerC = new Promise((resolve) => {
setTimeout(() => resolve('Data from Server C'), 1200);
});
console.log('Racing to get data...');
Promise.race([fetchFromServerA, fetchFromServerB, fetchFromServerC])
.then((winner) => {
console.log('The fastest data source:', winner);
})
.catch((error) => {
console.error('Race failed due to:', error);
});
// Expected Output after 0.3 seconds:
// Racing to get data...
// The fastest data source: Data from Server B
Here, fetchFromServerB resolves first, so Promise.race() immediately resolves with its value, and the other promises' outcomes are ignored by the .then() block.
Promise.race() Example: First to Reject Also Wins
It's important to remember that Promise.race() cares about the first promise to settle, whether that's a resolve or a reject.
const successfulFetch = new Promise((resolve) => {
setTimeout(() => resolve('Operation completed successfully!'), 1000);
});
const timeout = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('Operation timed out!')), 500); // This one wins by rejecting
});
console.log('Starting operation with a timeout...');
Promise.race([successfulFetch, timeout])
.then((result) => {
console.log('Operation result:', result);
})
.catch((error) => {
console.error('Operation error:', error.message);
});
// Expected Output after 0.5 seconds:
// Starting operation with a timeout...
// Operation error: Operation timed out!
Even though successfulFetch would eventually resolve, the timeout promise rejected first, making Promise.race() reject as well.
When to Use Promise.race()
- Timeouts: A common pattern is to race an actual operation against a timeout promise. If the timeout promise settles first, you can consider the operation failed due to taking too long.
- Redundancy/Failover: Querying multiple services for the same data and using the response from the quickest one.
- User Experience: Displaying a loading indicator only if an operation takes longer than a certain threshold (racing the operation against a short timer that resolves to
trueto show the spinner).
Choosing the Right Tool: Promise.all() vs. Promise.race()
The choice between these two powerful methods depends entirely on your specific requirements:
- Use
Promise.all()when:- You need all operations to succeed.
- You need to collect the results from all operations.
- You want to know if any operation failed.
- Use
Promise.race()when:- You only care about the first operation to complete (resolve or reject).
- You want to implement a timeout for an operation.
- You have multiple redundant sources and want the fastest response.
Mastering Asynchronous Control Flow
Promise.all() and Promise.race() are indispensable tools in modern JavaScript development. They provide elegant and efficient ways to manage multiple asynchronous operations, significantly simplifying complex control flow patterns.
By understanding their distinct behaviors and ideal use cases, you can write more robust, performant, and user-friendly applications. Practice integrating them into your projects, and you'll quickly appreciate the power they bring to asynchronous programming.