Introduction
In modern web development, especially in Node.js and frontend frameworks like Vue, handling asynchronous operations is a daily task.
While async/await has made asynchronous code much cleaner, many developers still fall into the trap of running independent operations one after another, creating unnecessary delays. This is where Promise combinators like Promise.all and Promise.allSettled come in.
In this article, we’ll explain what these combinators are, compare the slow sequential approach to the fast concurrent one, and show you when to use each to make your applications significantly faster and more resilient.
What are Promise Combinators?
Promise combinators are functions that take an array of promises and help you manage them as a single unit. Instead of waiting for each promise to finish individually, you can run them all at the same time (concurrently) and wait for a single, combined result.
Think of it like ordering at a restaurant with friends. Instead of one person ordering, waiting for their food, and only then the next person orders, everyone orders at the same time. The whole table gets their food much faster because the kitchen (the JavaScript engine) works on all the orders in parallel.
Promise.all vs. Promise.allSettled: When to Use Each?
This is the key distinction. Choosing the right one depends on how you want to handle failures.
Promise.all
Behavior: Waits for all promises in the array to be fulfilled (succeed). If even one promise is rejected (fails), the entire Promise.all immediately rejects with the reason of the first promise that failed. It's an "all or nothing" approach.
Use for: Critical, dependent tasks. For example, fetching user data and user permissions before rendering a page. If either fails, you can't proceed anyway.
Returns: An array of the fulfilled values, in the same order as the input promises.
Promise.allSettled
Behavior: Waits for all promises to be "settled" — meaning each one has either been fulfilled or rejected. It never rejects itself, even if some of the input promises fail.
Use for: Independent, non-critical tasks. For example, fetching data for multiple widgets on a dashboard. If one widget's data fails to load, you still want to display the others.
Returns: An array of objects, each describing the outcome of a promise ({status: 'fulfilled', value: ...} or {status: 'rejected', reason: ...}).
A Simple Rule to Remember
👉 Use Promise.all when all the tasks must succeed together. 👉 Use Promise.allSettled when you need the outcome of every task, regardless of success or failure.
Practical Examples
Let's imagine we're building a user dashboard and need to fetch three pieces of data: the user's profile, their notifications, and their recent activity. We'll create simple mock functions to simulate API calls with different delays.
// Helper functions to simulate API calls
const fetchUserProfile = () => new Promise(resolve => {
console.log('Fetching user profile...');
setTimeout(() => resolve({ id: 1, name: 'John Doe' }), 1000); // 1 second
});const fetchUserNotifications = () => new Promise(resolve => {
console.log('Fetching notifications...');
setTimeout(() => resolve(['New message', 'Friend request']), 1500); // 1.5 seconds
});const fetchUserActivity = () => new Promise(resolve => {
console.log('Fetching recent activity...');
setTimeout(() => resolve(['Logged in', 'Posted a comment']), 500); // 0.5 seconds
});
Example 1: The Slow Sequential Way (Less Ideal)
This is the common mistake—awaiting each promise one by one.
async function getDashboardDataSequential() {
console.time('Sequential');
const profile = await fetchUserProfile();
const notifications = await fetchUserNotifications();
const activity = await fetchUserActivity();console.log('Sequential Data:', { profile, notifications, activity });
console.timeEnd('Sequential');
}getDashboardDataSequential();
Sample Output & Explanation:
The operations run in a sequence. The total time is the sum of all individual delays.
Fetching user profile...(waits 1s)Fetching notifications...(waits 1.5s)Fetching recent activity...(waits 0.5s)Sequential Data: { profile: { id: 1, name: 'John Doe' }, ... }Sequential: 3012.55 ms
The total time is roughly 3 seconds (1s + 1.5s + 0.5s). The user is left waiting for all operations to complete one after another, even though they don't depend on each other.
Example 2: The Fast Concurrent Way with Promise.all (Optimized)
Here, we start all the promises at once and wait for them together.
async function getDashboardDataConcurrent() {
console.time('Concurrent with Promise.all');
const [profile, notifications, activity] = await Promise.all([
fetchUserProfile(),
fetchUserNotifications(),
fetchUserActivity()
]);console.log('Concurrent Data:', { profile, notifications, activity });
console.timeEnd('Concurrent with Promise.all');
}getDashboardDataConcurrent();
Sample Output & Explanation:
All three "Fetching..." logs appear almost instantly because all operations start at the same time. The total time is determined by the longest-running promise.
Fetching user profile...Fetching notifications...Fetching recent activity...(waits 1.5s)Concurrent Data: { profile: { id: 1, name: 'John Doe' }, ... }Concurrent with Promise.all: 1507.82 ms
The total time is now roughly 1.5 seconds, the duration of the slowest API call (fetchUserNotifications). This is a 50% performance improvement over the sequential method.
Example 3: The Resilient Way with Promise.allSettled
Now, let's pretend the notification service is down and will fail.
const fetchUserNotificationsFails = () => new Promise((_, reject) => {
console.log('Fetching notifications (will fail)...');
setTimeout(() => reject(new Error('Service Unavailable')), 1500);
});async function getDashboardDataResilient() {
console.time('Resilient with Promise.allSettled');const results = await Promise.allSettled([
fetchUserProfile(),
fetchUserNotificationsFails(),
fetchUserActivity()
]);
// You can now safely inspect each result
const successfulData = results
.filter(result => result.status === 'fulfilled')
.map(result => result.value);
const failedReasons = results
.filter(result => result.status === 'rejected')
.map(result => result.reason);console.log('Successful Data:', successfulData);
if (failedReasons.length > 0) {
console.error('Failed Operations:', failedReasons);
}console.timeEnd('Resilient with Promise.allSettled');
}getDashboardDataResilient();
Sample Output & Explanation:
If we had used Promise.all, the entire block would have failed. But with Promise.allSettled, we get a detailed report of what succeeded and what failed, allowing us to build a more robust UI.
Fetching user profile...Fetching notifications (will fail)...Fetching recent activity...(waits 1.5s)Successful Data: [{ id: 1, name: 'John Doe' },[ 'Logged in', 'Posted a comment' ]]Failed Operations: [ Error: Service Unavailable ]Resilient with Promise.allSettled: 1509.11 ms
The total time is still fast (1.5 seconds), but our application didn't crash. We successfully loaded the profile and activity data and can now gracefully inform the user that notifications couldn't be loaded.
Key Points for Using Promise Combinators
Run Independent Tasks Concurrently: If a set of async tasks don't depend on each other's results, always run them concurrently for a massive performance boost.
Promise.all for All-or-Nothing: Use it when the entire operation is invalid if any single part fails. It simplifies success handling since you get a clean array of results.
Promise.allSettled for Resilience: Use it when you want to get as much data as possible, even if some sources fail. It's perfect for dashboards, aggregators, or any UI where partial data is better than a full-page error.
Input is an Array of Promises: Both functions take a single argument: an iterable (usually an array) of promises.
Output is a Single Promise: They return a new, single promise that you can await, which will then resolve to an array of results.
Conclusion
Understanding and using Promise.all and Promise.allSettled is a critical skill for any modern JavaScript developer. By shifting from a sequential mindset to a concurrent one, you can dramatically reduce load times and improve the user experience. Choose the right tool for the job—Promise.all for critical dependencies and Promise.allSettled for fault-tolerant independence—and your applications will become faster and more robust.