How to Use the Promise API for Concurrency Patterns in JavaScript
When you have multiple asynchronous operations to run, you rarely want to execute them one after another. Fetching a user's profile, their orders, and their notifications can happen simultaneously. But how you combine those promises depends on what you need: should everything succeed? Should you wait for all results regardless of failures? Do you only need the fastest response?
JavaScript provides a set of static methods on the Promise constructor that handle these concurrency patterns. Each method takes a collection of promises and returns a single promise that resolves or rejects based on different rules. Choosing the right one is the difference between robust parallel code and subtle bugs.
This guide covers every Promise static method with clear examples, shows you when to use each one, and provides a complete comparison to help you pick the right tool for each situation.
Promise.all(iterable): Wait for All (Fail-Fast)
Promise.all() takes an iterable of promises and returns a single promise that resolves when every promise in the iterable has fulfilled. The result is an array of all the resolved values, in the same order as the input.
If any promise rejects, Promise.all() immediately rejects with that error, without waiting for the others to settle. This is the "fail-fast" behavior.
Basic Usage
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);
const results = await Promise.all([promise1, promise2, promise3]);
console.log(results); // [1, 2, 3]
Real-World Example: Fetching Multiple Resources in Parallel
async function loadDashboard(userId) {
const [user, orders, notifications] = await Promise.all([
fetch(`/api/users/${userId}`).then(r => r.json()),
fetch(`/api/users/${userId}/orders`).then(r => r.json()),
fetch(`/api/users/${userId}/notifications`).then(r => r.json())
]);
console.log("User:", user.name);
console.log("Orders:", orders.length);
console.log("Notifications:", notifications.length);
return { user, orders, notifications };
}
All three fetches start simultaneously. Promise.all() waits until all three complete, then returns their results in order. This is much faster than fetching them sequentially:
// SEQUENTIAL: slow, each waits for the previous one
const user = await fetch(`/api/users/${userId}`).then(r => r.json());
const orders = await fetch(`/api/users/${userId}/orders`).then(r => r.json());
const notifications = await fetch(`/api/users/${userId}/notifications`).then(r => r.json());
// PARALLEL with Promise.all: all three run at the same time
const [user, orders, notifications] = await Promise.all([
fetch(`/api/users/${userId}`).then(r => r.json()),
fetch(`/api/users/${userId}/orders`).then(r => r.json()),
fetch(`/api/users/${userId}/notifications`).then(r => r.json())
]);
Fail-Fast Behavior
The moment any single promise rejects, Promise.all() rejects immediately:
const results = Promise.all([
Promise.resolve("success 1"),
Promise.reject(new Error("failed!")), // This rejects
Promise.resolve("success 3") // This resolve is ignored
]);
try {
await results;
} catch (err) {
console.log(err.message); // "failed!"
// We only get the first rejection: no access to "success 1" or "success 3"
}
This is both a feature and a limitation. If all operations are equally critical and any failure means the whole batch is useless, fail-fast is exactly what you want. If you need results from the operations that succeeded, use Promise.allSettled() instead.
Order Is Preserved
Results always match the input order, regardless of which promise resolves first:
const results = await Promise.all([
new Promise(resolve => setTimeout(() => resolve("slow"), 300)),
new Promise(resolve => setTimeout(() => resolve("fast"), 100)),
new Promise(resolve => setTimeout(() => resolve("medium"), 200))
]);
console.log(results); // ["slow", "fast", "medium"] (matches input order, not completion order)
Non-Promise Values Are Wrapped
Promise.all() wraps non-promise values in Promise.resolve() automatically:
const results = await Promise.all([
42, // Wrapped in Promise.resolve(42)
"hello", // Wrapped in Promise.resolve("hello")
Promise.resolve(true), // Already a promise
fetch("/api/data").then(r => r.json())
]);
console.log(results); // [42, "hello", true, { ...apiData }]
Empty Iterable
Passing an empty array resolves immediately with an empty array:
const results = await Promise.all([]);
console.log(results); // []
Common Pattern: Parallel Processing with Map
const userIds = [1, 2, 3, 4, 5];
const users = await Promise.all(
userIds.map(id => fetch(`/api/users/${id}`).then(r => r.json()))
);
console.log(users); // Array of 5 user objects
Be careful with large arrays. If you pass 1000 URLs to Promise.all(urls.map(url => fetch(url))), you will fire 1000 simultaneous HTTP requests, which can overwhelm the server or hit rate limits. For large batches, process in chunks:
async function batchProcess(items, batchSize, fn) {
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(fn));
results.push(...batchResults);
}
return results;
}
const allUsers = await batchProcess(
userIds,
10, // Process 10 at a time
id => fetch(`/api/users/${id}`).then(r => r.json())
);
Promise.allSettled(iterable): Wait for All (No Fail)
Promise.allSettled() waits for every promise to settle (either fulfill or reject) and never short-circuits. It returns an array of result objects describing the outcome of each promise.
Each result object has a status property that is either "fulfilled" or "rejected":
- Fulfilled:
{ status: "fulfilled", value: result } - Rejected:
{ status: "rejected", reason: error }
Basic Usage
const results = await Promise.allSettled([
Promise.resolve("success"),
Promise.reject(new Error("failed")),
Promise.resolve(42)
]);
console.log(results);
// [
// { status: "fulfilled", value: "success" },
// { status: "rejected", reason: Error: "failed" },
// { status: "fulfilled", value: 42 }
// ]
Unlike Promise.all(), the rejection of one promise does not prevent you from getting the results of the others.
When to Use allSettled Instead of all
Use Promise.allSettled() when individual failures are acceptable and you want results from whatever succeeded:
async function loadDashboardResilient(userId) {
const results = await Promise.allSettled([
fetch(`/api/users/${userId}`).then(r => r.json()),
fetch(`/api/users/${userId}/orders`).then(r => r.json()),
fetch(`/api/users/${userId}/notifications`).then(r => r.json())
]);
const [userResult, ordersResult, notificationsResult] = results;
const user = userResult.status === "fulfilled"
? userResult.value
: null;
const orders = ordersResult.status === "fulfilled"
? ordersResult.value
: [];
const notifications = notificationsResult.status === "fulfilled"
? notificationsResult.value
: [];
if (!user) {
throw new Error("Critical: could not load user data");
}
return { user, orders, notifications };
}
The dashboard loads even if orders or notifications fail. Only the user fetch is treated as critical.
Separating Successes and Failures
async function fetchMultipleURLs(urls) {
const results = await Promise.allSettled(
urls.map(url => fetch(url).then(r => r.json()))
);
const successes = results
.filter(r => r.status === "fulfilled")
.map(r => r.value);
const failures = results
.filter(r => r.status === "rejected")
.map((r, i) => ({
url: urls[results.indexOf(r)],
error: r.reason.message
}));
console.log(`${successes.length} succeeded, ${failures.length} failed`);
if (failures.length > 0) {
console.warn("Failed URLs:", failures);
}
return { successes, failures };
}
Batch Operations with Error Reporting
async function sendNotifications(users) {
const results = await Promise.allSettled(
users.map(user =>
sendEmail(user.email, "Welcome!")
.then(() => ({ userId: user.id, sent: true }))
)
);
const report = {
total: results.length,
sent: results.filter(r => r.status === "fulfilled").length,
failed: results
.map((r, i) => r.status === "rejected" ? { user: users[i], error: r.reason } : null)
.filter(Boolean)
};
console.log(`Sent: ${report.sent}/${report.total}`);
if (report.failed.length > 0) {
console.warn("Failed to send to:", report.failed.map(f => f.user.email));
// Queue failed ones for retry
await queueForRetry(report.failed);
}
return report;
}
Promise.race(iterable): First to Settle
Promise.race() returns a promise that settles as soon as any promise in the iterable settles. It adopts the state (fulfilled or rejected) and value of the first promise to finish. All remaining promises continue to run but their results are ignored.
Basic Usage
const result = await Promise.race([
new Promise(resolve => setTimeout(() => resolve("slow"), 300)),
new Promise(resolve => setTimeout(() => resolve("fast"), 100)),
new Promise(resolve => setTimeout(() => resolve("medium"), 200))
]);
console.log(result); // "fast" (the first to resolve)
Race Includes Rejections
If the first promise to settle is a rejection, Promise.race() rejects:
try {
const result = await Promise.race([
new Promise((_, reject) => setTimeout(() => reject(new Error("quick fail")), 50)),
new Promise(resolve => setTimeout(() => resolve("slow success"), 200))
]);
} catch (err) {
console.log(err.message); // "quick fail" (rejection won the race9
}
Classic Use Case: Timeout Pattern
The most common use of Promise.race() is implementing timeouts for operations that do not have built-in timeout support:
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Operation timed out after ${ms}ms`)), ms)
);
return Promise.race([promise, timeout]);
}
// Usage
try {
const data = await withTimeout(
fetch("/api/slow-endpoint").then(r => r.json()),
5000 // 5 second timeout
);
console.log("Data:", data);
} catch (err) {
if (err.message.includes("timed out")) {
console.log("Request took too long");
} else {
console.log("Request failed:", err.message);
}
}
Improved Timeout with AbortController
The basic timeout pattern has a flaw: even after the timeout fires, the original fetch continues running in the background. Using AbortController solves this:
async function fetchWithTimeout(url, timeoutMs) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
return await response.json();
} catch (err) {
clearTimeout(timeoutId);
if (err.name === "AbortError") {
throw new Error(`Request to ${url} timed out after ${timeoutMs}ms`);
}
throw err;
}
}
First Available Server
async function fetchFromFastest(urls) {
return Promise.race(
urls.map(url =>
fetch(url).then(response => {
if (!response.ok) {
throw new Error(`${url} returned ${response.status}`);
}
return response.json();
})
)
);
}
// Use whichever mirror responds first
const data = await fetchFromFastest([
"https://api-us.example.com/data",
"https://api-eu.example.com/data",
"https://api-asia.example.com/data"
]);
With Promise.race(), if the fastest server responds with an error, the race is lost even though other servers might succeed. If you want the first successful response, use Promise.any() instead.
Empty Iterable
Promise.race([]) returns a promise that never settles. It stays pending forever:
const neverSettles = Promise.race([]);
// This promise will never resolve or reject
Promise.any(iterable): First to Fulfill
Promise.any() returns a promise that resolves as soon as any promise fulfills. It ignores rejections unless every promise rejects, in which case it rejects with an AggregateError containing all the rejection reasons.
Think of it as the optimistic counterpart to Promise.race(): it keeps hoping for success until all hope is lost.
Basic Usage
const result = await Promise.any([
Promise.reject(new Error("first failed")),
Promise.resolve("second succeeded"),
Promise.resolve("third succeeded")
]);
console.log(result); // "second succeeded" (first fulfillment wins)
Rejections before the first fulfillment are silently ignored:
const result = await Promise.any([
new Promise((_, reject) => setTimeout(() => reject(new Error("fail 1")), 100)),
new Promise((_, reject) => setTimeout(() => reject(new Error("fail 2")), 200)),
new Promise(resolve => setTimeout(() => resolve("success!"), 300))
]);
console.log(result); // "success!" (waited through two rejections)
All Promises Reject: AggregateError
When every promise rejects, Promise.any() throws an AggregateError, which contains an errors array with all individual rejection reasons:
try {
await Promise.any([
Promise.reject(new Error("Database down")),
Promise.reject(new Error("Cache miss")),
Promise.reject(new Error("API timeout"))
]);
} catch (err) {
console.log(err instanceof AggregateError); // true
console.log(err.message); // "All promises were rejected"
console.log(err.errors);
// [
// Error: "Database down",
// Error: "Cache miss",
// Error: "API timeout"
// ]
// Inspect each failure
err.errors.forEach((error, i) => {
console.log(`Source ${i + 1} failed: ${error.message}`);
});
}
Use Case: Redundant Data Sources
Promise.any() is perfect when you have multiple sources for the same data and just need one to work:
async function getUserData(userId) {
return Promise.any([
// Try primary database
fetchFromPrimaryDB(userId)
.then(data => ({ source: "primary", data })),
// Try read replica
fetchFromReplica(userId)
.then(data => ({ source: "replica", data })),
// Try cache
fetchFromCache(userId)
.then(data => ({ source: "cache", data }))
]);
}
try {
const result = await getUserData(42);
console.log(`Got data from ${result.source}:`, result.data);
} catch (err) {
// Only reaches here if ALL three sources failed
console.error("All data sources failed:", err.errors.map(e => e.message));
}
Promise.any() vs. Promise.race()
The critical difference is how they handle rejections:
const sources = [
new Promise((_, reject) => setTimeout(() => reject(new Error("fast fail")), 50)),
new Promise(resolve => setTimeout(() => resolve("slow success"), 200))
];
// Promise.race: the fast rejection wins
try {
await Promise.race(sources);
} catch (err) {
console.log("race result:", err.message); // "fast fail"
}
// Promise.any: ignores the rejection, waits for fulfillment
const result = await Promise.any(sources);
console.log("any result:", result); // "slow success"
Promise.race() resolves or rejects with whoever finishes first. Promise.any() only resolves with the first fulfillment, ignoring rejections along the way.
Service Health Check
async function findHealthyService(endpoints) {
try {
const healthy = await Promise.any(
endpoints.map(async endpoint => {
const response = await fetch(`${endpoint}/health`, {
signal: AbortSignal.timeout(3000)
});
if (!response.ok) {
throw new Error(`${endpoint} returned ${response.status}`);
}
return endpoint; // Return the URL of the healthy service
})
);
console.log("Using healthy service:", healthy);
return healthy;
} catch (err) {
console.error("No healthy services found");
console.error("Failures:", err.errors.map(e => e.message));
throw new Error("All services are down");
}
}
Promise.resolve(value) and Promise.reject(reason)
These are utility methods for creating promises that are already settled. They are the simplest way to wrap a value in a promise or create a rejection.
Promise.resolve(value)
Creates a promise that is immediately fulfilled with the given value:
const p = Promise.resolve(42);
const value = await p;
console.log(value); // 42
Common uses:
// 1. Normalize a function that might return a value or a promise
function getData(useCache) {
if (useCache) {
return Promise.resolve(cachedData); // Synchronous value wrapped in a promise
}
return fetch("/api/data").then(r => r.json()); // Already a promise
}
// Caller always gets a promise regardless of the code path
const data = await getData(true);
// 2. Start a promise chain from a plain value
Promise.resolve(userData)
.then(user => validateUser(user))
.then(user => saveUser(user))
.catch(err => console.error(err));
// 3. Provide default values in parallel operations
const results = await Promise.all([
fetchCriticalData(),
fetchOptionalData().catch(() => Promise.resolve(defaultData))
]);
Special behavior with thenables:
If you pass a "thenable" (an object with a .then() method) to Promise.resolve(), it unwraps it and follows the thenable:
const thenable = {
then(resolve, reject) {
resolve(42);
}
};
const value = await Promise.resolve(thenable);
console.log(value); // 42 (unwrapped from the thenable)
If you pass an existing promise, it returns that same promise (not a new wrapper):
const original = new Promise(resolve => resolve(42));
const same = Promise.resolve(original);
console.log(original === same); // true (same object reference)
Promise.reject(reason)
Creates a promise that is immediately rejected with the given reason:
try {
await Promise.reject(new Error("Something failed"));
} catch (err) {
console.log(err.message); // "Something failed"
}
Common uses:
// 1. Early exit from a function that returns a promise
function loadUser(id) {
if (!id) {
return Promise.reject(new Error("User ID is required"));
}
return fetch(`/api/users/${id}`).then(r => r.json());
}
// 2. Providing fallback rejection in chains
const value = await somePromise
.then(result => result || Promise.reject(new Error("Empty result")));
// 3. Testing error handlers
function testErrorHandling() {
return Promise.reject(new Error("Simulated failure"))
.catch(err => {
console.log("Handler works:", err.message);
return "recovered";
});
}
Important: Unlike Promise.resolve(), Promise.reject() does not unwrap thenables or promises. Whatever you pass as the reason is used as-is:
const innerPromise = Promise.resolve(42);
const rejected = Promise.reject(innerPromise);
try {
await rejected;
} catch (err) {
console.log(err); // Promise { 42 } (the promise itself is the rejection reason)
console.log(err instanceof Promise); // true
}
Always pass Error objects to Promise.reject(), not strings or plain values. Error objects provide stack traces that are invaluable for debugging:
// BAD: no stack trace
return Promise.reject("something failed");
// GOOD: full stack trace
return Promise.reject(new Error("something failed"));
Promise.withResolvers() (ES2024)
Promise.withResolvers() is a convenience method added in ES2024 that creates a promise and exposes its resolve and reject functions externally. This solves a common pattern where you need to control a promise's settlement from outside the executor function.
The Problem It Solves
Before Promise.withResolvers(), extracting resolve and reject required an awkward pattern:
// The old way: extracting resolve/reject manually
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
// Now resolve and reject can be called from outside
setTimeout(() => resolve("done"), 1000);
const result = await promise; // "done"
This works but is clunky and requires declaring variables in the outer scope before the promise constructor.
The Clean Solution
const { promise, resolve, reject } = Promise.withResolvers();
// resolve and reject are already available
setTimeout(() => resolve("done"), 1000);
const result = await promise;
console.log(result); // "done"
Use Case: Event-Based Resolution
This is particularly useful when a promise's settlement is triggered by an event or callback that is registered separately:
function waitForEvent(element, eventName, timeout = 5000) {
const { promise, resolve, reject } = Promise.withResolvers();
const handler = (event) => {
cleanup();
resolve(event);
};
const timeoutId = setTimeout(() => {
cleanup();
reject(new Error(`Timeout waiting for "${eventName}" event`));
}, timeout);
function cleanup() {
element.removeEventListener(eventName, handler);
clearTimeout(timeoutId);
}
element.addEventListener(eventName, handler);
return promise;
}
// Usage
const clickEvent = await waitForEvent(document.getElementById("btn"), "click");
console.log("Button was clicked:", clickEvent);
Use Case: Queue with Async Consumers
class AsyncQueue {
#queue = [];
#waiters = [];
enqueue(item) {
if (this.#waiters.length > 0) {
// Someone is waiting: resolve their promise immediately
const { resolve } = this.#waiters.shift();
resolve(item);
} else {
this.#queue.push(item);
}
}
dequeue() {
if (this.#queue.length > 0) {
return Promise.resolve(this.#queue.shift());
}
// Nothing in queue: create a promise that will resolve when something arrives
const { promise, resolve, reject } = Promise.withResolvers();
this.#waiters.push({ resolve, reject });
return promise;
}
}
const queue = new AsyncQueue();
// Consumer waits for items
(async () => {
console.log("Waiting for item...");
const item = await queue.dequeue();
console.log("Got:", item);
})();
// Producer adds item later
setTimeout(() => queue.enqueue("hello"), 1000);
// After 1 second: "Got: hello"
Use Case: Deferred Promise Pattern
function createDeferred() {
const { promise, resolve, reject } = Promise.withResolvers();
return {
promise,
resolve,
reject,
// Convenience methods
then: promise.then.bind(promise),
catch: promise.catch.bind(promise),
finally: promise.finally.bind(promise)
};
}
// Use it to coordinate between different parts of your code
const dataReady = createDeferred();
// Part 1: set up the listener
dataReady.then(data => {
console.log("Data arrived:", data);
updateUI(data);
});
// Part 2: somewhere else, when data becomes available
fetchData().then(data => {
dataReady.resolve(data); // Triggers the handler in Part 1
});
Comparison Table: all vs. allSettled vs. race vs. any
Here is the definitive reference for choosing the right concurrency method:
| Method | Resolves When | Rejects When | Result Shape | Short-Circuits? |
|---|---|---|---|---|
Promise.all | All fulfill | Any rejects | [value, value, ...] | Yes, on first rejection |
Promise.allSettled | All settle | Never | [{status, value/reason}, ...] | No, always waits for all |
Promise.race | First settles | First settles (if rejection) | Single value or error | Yes, on first settlement |
Promise.any | First fulfills | All reject | Single value or AggregateError | Yes, on first fulfillment |
Decision Flowchart
Do you need ALL results?
├── Yes → Can any failure be tolerated?
│ ├── No → Promise.all() (fail-fast, all-or-nothing)
│ └── Yes → Promise.allSettled() (get every result regardless)
│
└── No → Do you need the first SUCCESS specifically?
├── Yes → Promise.any() (first to fulfill, ignores rejections)
└── No → Promise.race() (first to settle, win or lose)
Side-by-Side Behavior
const promises = [
new Promise((_, reject) => setTimeout(() => reject(new Error("A failed")), 100)),
new Promise(resolve => setTimeout(() => resolve("B succeeded"), 200)),
new Promise(resolve => setTimeout(() => resolve("C succeeded"), 300))
];
// Promise.all: rejects immediately when A fails at 100ms
try {
await Promise.all(promises);
} catch (err) {
console.log("all:", err.message);
// "A failed": got nothing, even though B and C would succeed
}
// Promise.allSettled: waits for everything, returns at 300ms
const settled = await Promise.allSettled(promises);
console.log("allSettled:", settled.map(r => r.status));
// ["rejected", "fulfilled", "fulfilled"]
// Promise.race: settles with A's rejection at 100ms
try {
await Promise.race(promises);
} catch (err) {
console.log("race:", err.message);
// "A failed" (first to settle was a rejection)
}
// Promise.any: skips A's rejection, resolves with B at 200ms
const first = await Promise.any(promises);
console.log("any:", first);
// "B succeeded" (first fulfillment)
Real-World Scenarios
// Scenario 1: Load all required page data → Promise.all
const [header, content, footer] = await Promise.all([
loadHeader(),
loadContent(),
loadFooter()
]);
// Page cannot render without any section → fail-fast is correct
// Scenario 2: Send analytics to multiple services → Promise.allSettled
await Promise.allSettled([
sendToGoogleAnalytics(event),
sendToMixpanel(event),
sendToCustomService(event)
]);
// Don't care if some analytics fail → get status of each
// Scenario 3: Implement request timeout → Promise.race
const data = await Promise.race([
fetchData(),
timeout(5000)
]);
// Whichever settles first wins
// Scenario 4: Try multiple CDN sources → Promise.any
const asset = await Promise.any([
loadFromCDN1(assetUrl),
loadFromCDN2(assetUrl),
loadFromCDN3(assetUrl)
]);
// Just need one to work → ignore individual failures
Edge Cases with Empty Iterables
| Method | Empty Iterable Result |
|---|---|
Promise.all([]) | Resolves immediately with [] |
Promise.allSettled([]) | Resolves immediately with [] |
Promise.race([]) | Never settles (pending forever) |
Promise.any([]) | Rejects with AggregateError (no promises to fulfill) |
await Promise.all([]); // []
await Promise.allSettled([]); // []
// Promise.race([]) hangs forever - be careful!
// Promise.any([]) throws AggregateError
try {
await Promise.any([]);
} catch (err) {
console.log(err instanceof AggregateError); // true
console.log(err.errors.length); // 0
}
Summary
The Promise static methods give you precise control over how multiple asynchronous operations combine. Each method answers a different question about your concurrent operations.
| Method | Question It Answers |
|---|---|
Promise.all() | Did everything succeed? |
Promise.allSettled() | What happened to each one? |
Promise.race() | Which one finished first? |
Promise.any() | Which one succeeded first? |
Promise.resolve() | Wrap this value in a fulfilled promise |
Promise.reject() | Wrap this reason in a rejected promise |
Promise.withResolvers() | Give me a promise I can control externally |
Key rules to remember:
- Use
Promise.all()when all operations are required and any failure is fatal - Use
Promise.allSettled()when you want results from everything, regardless of individual failures - Use
Promise.race()for timeouts and "first response wins" scenarios - Use
Promise.any()for redundant sources where you need just one success Promise.race([])never settles. Always ensure the iterable is non-empty.Promise.all()andPromise.any()short-circuit. Other promises keep running in the background, but their results are ignored.- All four methods preserve input order in their results (except
raceandany, which return a single value) - Always pass
Errorobjects toPromise.reject(), not strings