Aggregate multiple API requests with Promise.all()
Learn how Promise.all() works in JavaScript with real examples - concurrent API requests, error handling, Promise.allSettled() comparison, React Server Components, and concurrency limiting with p-limit.
When a page needs data from several sources at the same time, the natural instinct is to write one await after another. Each request runs, finishes, then the next one starts. The total wait time adds up to the sum of every individual call. That is a waterfall, and it is one of the most common performance problems in JavaScript applications.
Promise.all() removes the waterfall by firing multiple async operations simultaneously and collecting all the results once they finish. This guide covers how Promise.all() works, how its rejection behavior affects error handling, when Promise.allSettled() is the better choice, how it fits into React Server Components, and how to control concurrency when running large batches.
What is Promise.all()?
Promise.all() is a built-in JavaScript method that takes an iterable of promises and returns a single promise. That returned promise resolves when every input promise has fulfilled, producing an array of results in the same order as the inputs. If any single promise rejects, Promise.all() rejects immediately and discards the results of any already-resolved promises. Promise.all() has been part of JavaScript since ES2015 and is marked Baseline: Widely Available on MDN. There are no compatibility concerns in any modern environment.
The over-awaiting problem
The most common misuse of async/await is declaring promises in separate lines before combining them. Each await in sequence creates a waterfall where none needed to exist.
// Waterfall — each request waits for the previous one to finish
const users = await fetch('/api/users').then(r => r.json());
const products = await fetch('/api/products').then(r => r.json());
const orders = await fetch('/api/orders').then(r => r.json());
// Three 300ms requests = ~900ms total
Promise.all() starts all three at once. The total time becomes the duration of the slowest single request.
// Concurrent — all three requests fire simultaneously
const [users, products, orders] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/products').then(r => r.json()),
fetch('/api/orders').then(r => r.json())
]);
// Three 300ms requests = ~300ms total
A subtler version of the same mistake appears when promises are assigned to variables before being passed to Promise.all(). The promises start when you call the function, not when you await them, so this pattern is fine:
// This is correct — promises start immediately on assignment
const usersPromise = fetch('/api/users').then(r => r.json());
const productsPromise = fetch('/api/products').then(r => r.json());
const [users, products] = await Promise.all([usersPromise, productsPromise]);
The problem only occurs when you await each variable before reaching Promise.all().
Using Promise.all() with Axios
Axios returns promises directly, so there is no .json() call needed after each request. The axios.all method was deprecated in favor of native Promise.all and should not be used in new code.
import axios from 'axios';
const fetchDashboardData = async () => {
try {
const [usersRes, productsRes, ordersRes] = await Promise.all([
axios.get('/api/users'),
axios.get('/api/products'),
axios.get('/api/orders')
]);
return {
users: usersRes.data,
products: productsRes.data,
orders: ordersRes.data
};
} catch (error) {
throw new Error('One or more requests failed');
}
};
How rejection works in Promise.all()
Promise.all() uses fail-fast behavior. The moment any one promise in the array rejects, the entire Promise.all() rejects immediately. It does not wait for the remaining promises to settle.
const results = await Promise.all([
Promise.resolve('user data'),
Promise.reject(new Error('products API down')),
Promise.resolve('order data')
]);
// Throws: Error: products API down
// 'user data' and 'order data' are discarded
This behavior is correct when all data is required before rendering. A dashboard that needs every panel populated before showing anything is a good fit. When individual failures are acceptable, Promise.allSettled() is the right choice.
When to use Promise.allSettled()
Promise.allSettled() was introduced in ES2020 and is fully supported across all modern environments. It runs every promise to completion regardless of individual failures and returns an array of result objects, each with a status of either "fulfilled" or "rejected" along with the resolved value or rejection reason.
const results = await Promise.allSettled([
fetch('/api/users').then(r => r.json()),
fetch('/api/products').then(r => r.json()),
fetch('/api/orders').then(r => r.json())
]);
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log(result.value);
} else {
console.error('Failed:', result.reason);
}
});
Use allSettled when each result can stand on its own: a page where widgets load independently, a batch file processor where partial success is acceptable, or a reporting tool collecting from multiple third-party services.
Choosing between Promise methods
| Method | Resolves when | Rejects when | Best for |
|---|---|---|---|
Promise.all() |
All promises fulfill | Any promise rejects | Need every result; one failure should abort |
Promise.allSettled() |
All promises settle | Never rejects | Each result handled independently |
Promise.race() |
First promise settles | First promise rejects | Timeout patterns; first-available response |
Promise.any() |
First promise fulfills | All promises reject | Redundancy; first success wins |
Promise.all() in React server components
In async React Server Components, sequential await calls create the same waterfall as in the browser. The React team explicitly recommends Promise.all() for parallel data fetching inside async Server Components.
// app/dashboard/page.jsx — waterfall
export default async function Dashboard() {
const users = await fetchUsers();
const stats = await fetchStats();
return <DashboardView users={users} stats={stats} />;
}
// app/dashboard/page.jsx — concurrent
export default async function Dashboard() {
const [users, stats] = await Promise.all([fetchUsers(), fetchStats()]);
return <DashboardView users={users} stats={stats} />;
}
Total latency drops from the sum of both fetch durations to the duration of the slower one. On a dashboard with four or five data sources, this difference is large enough to affect Core Web Vitals scores.
Handling individual errors without aborting the batch
When you need concurrent execution with per-item error handling, catch errors on each promise before passing them into Promise.all(). This prevents a single failure from rejecting the entire batch.
const safeRequest = (url) =>
fetch(url)
.then(r => r.json())
.catch(err => ({ error: err.message, url }));
const results = await Promise.all(urls.map(safeRequest));
// Always resolves. Each item is either data or { error: '...', url: '...' }
Limiting concurrency for large batches
Firing hundreds of requests simultaneously can hit API rate limits or overload a server. The p-limit package sets a ceiling on how many promises run at any one time.
npm install p-limit
import pLimit from 'p-limit';
const limit = pLimit(3);
const urls = ['/api/item/1', '/api/item/2', '/api/item/3', '/api/item/4', '/api/item/5'];
const results = await Promise.all(
urls.map(url => limit(() => fetch(url).then(r => r.json())))
);
No more than three requests run at a time. As one slot frees up, the next request starts. All results still resolve into a single array in input order.
Processing large arrays in chunks
When p-limit is not an option or you need to process a large dataset in sequential batches rather than a rolling concurrency window, slice the array into chunks and process each chunk with Promise.all().
async function processInBatches(items, batchSize, processFn) {
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchResults = await Promise.all(batch.map(processFn));
results.push(...batchResults);
}
return results;
}
// Send emails to 50,000 users, 100 at a time
await processInBatches(users, 100, sendWelcomeEmail);
Each batch of 100 runs concurrently. The next batch does not start until the current one completes. This gives predictable server load and avoids exhausting connection pools.
You can see the core Promise.all() examples running in this CodeSandbox.
Other promise methods worth exploring:
FAQs
1, Does Promise.all() run requests in parallel?
Promise.all() starts all promises at the same time. For network requests in a browser or Node.js, they run concurrently. The total time is determined by the slowest request, not the sum of all requests.
2, Does the order of results match the order of inputs?
Yes. The results array always matches the order of the input array regardless of which promise resolved first.
3, What happens if you pass non-promise values to Promise.all()?
Non-promise values are treated as already-resolved promises. Promise.all([1, 2, 3]) resolves immediately with [1, 2, 3]. This is useful when some items in a dynamic array may already be resolved values rather than pending promises.
4, Can I use Promise.all() with an empty array?
Yes. Promise.all([]) resolves synchronously with an empty array []. It is the only case where Promise.all() resolves synchronously. All other inputs resolve asynchronously, even if every promise in the array is already fulfilled.
5, What is the difference between Promise.all() and async/await?
They are not alternatives. async/await is syntax for writing promise-based code. Promise.all() is a method for running multiple promises concurrently. You use both together: await Promise.all([...]).
6, Does TanStack Query replace Promise.all()?
TanStack Query handles client-side parallel queries with caching and background refetching through useQueries. It is the right tool for client-side data management in React. Promise.all() is the right choice for server-side logic, utility functions, async Server Components, and any context where a query library is not involved.
7, What happens to resolved results when one promise rejects in Promise.all()?
They are discarded. Promise.all() rejects with the first rejection reason and does not wait for the others to settle. Use Promise.allSettled() if you need all results regardless of individual failures.