Promise Method Recap
Before diving into practical patterns, you need a solid understanding of the differences between Promise.all, Promise.allSettled, Promise.race, and Promise.any. Each method combines multiple Promises differently.
// Helper to create succeeding/failing Promises
const success = (value, ms) =>
new Promise((resolve) => setTimeout(() => resolve(value), ms));
const fail = (reason, ms) =>
new Promise((_, reject) => setTimeout(() => reject(reason), ms));
// Promise.all — all must succeed (rejects immediately if any fails)
async function allExample() {
try {
const results = await Promise.all([
success("A", 100),
success("B", 200),
fail("C failed", 150), // This causes the entire call to fail
]);
} catch (error) {
console.log("all failed:", error); // all failed: C failed
}
}
// Promise.allSettled — waits until all complete (continues on failure)
async function allSettledExample() {
const results = await Promise.allSettled([
success("A", 100),
fail("B failed", 200),
success("C", 150),
]);
results.forEach((result) => {
if (result.status === "fulfilled") {
console.log("Success:", result.value);
} else {
console.log("Failed:", result.reason);
}
});
// Success: A
// Failed: B failed
// Success: C
}
// Promise.race — returns the first to complete
// Promise.any — returns the first to succeed
| Method | On single failure | On all failure | Returns |
|---|---|---|---|
all | Rejects immediately | Rejects | Result array |
allSettled | Continues | Returns all results | Status+value array |
race | Rejects if failure comes first | Rejects | First result |
any | Continues | AggregateError | First success |
Concurrency Limiting — Promise Pool
Running hundreds of async tasks simultaneously can overload the server. A Pool pattern that limits concurrent execution is needed.
async function promisePool(tasks, concurrency = 5) {
/**
* Executes Promises with a concurrency limit.
* @param {Function[]} tasks - Array of functions in () => Promise form
* @param {number} concurrency - Maximum number of concurrent tasks
* @returns {Promise<any[]>} Result array (preserves input order)
*/
const results = new Array(tasks.length);
let currentIndex = 0;
async function worker() {
while (currentIndex < tasks.length) {
const index = currentIndex++;
try {
results[index] = await tasks[index]();
} catch (error) {
results[index] = { error };
}
}
}
// Create workers equal to the concurrency limit
const workers = Array.from(
{ length: Math.min(concurrency, tasks.length) },
() => worker()
);
await Promise.all(workers);
return results;
}
// Usage: execute 100 API requests, 5 at a time
async function fetchAllUsers() {
const userIds = Array.from({ length: 100 }, (_, i) => i + 1);
const tasks = userIds.map(
(id) => () => fetchUser(id) // () => Promise form
);
console.time("100 requests");
const results = await promisePool(tasks, 5);
console.timeEnd("100 requests");
// 100 requests: 2034ms (5 concurrent, assuming 100ms per request)
const succeeded = results.filter((r) => !r?.error).length;
console.log(`Succeeded: ${succeeded}, Failed: ${100 - succeeded}`);
}
// Simulation fetch function
async function fetchUser(id) {
await new Promise((r) => setTimeout(r, 100));
if (id % 17 === 0) throw new Error(`User ${id} not found`);
return { id, name: `User_${id}` };
}
Retry Pattern — Exponential Backoff
A pattern for retrying with exponential backoff when network requests fail temporarily.
async function withRetry(fn, options = {}) {
/**
* Retries with exponential backoff on failure.
* @param {Function} fn - Async function to execute
* @param {object} options - Retry configuration
*/
const {
maxRetries = 3, // Maximum retry count
baseDelay = 1000, // Base delay (ms)
maxDelay = 30000, // Maximum delay (ms)
backoffFactor = 2, // Backoff multiplier
shouldRetry = () => true, // Function to determine whether to retry
} = options;
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn(attempt);
} catch (error) {
lastError = error;
if (attempt === maxRetries || !shouldRetry(error)) {
break;
}
// Exponential backoff + jitter
const delay = Math.min(
baseDelay * Math.pow(backoffFactor, attempt),
maxDelay
);
const jitter = delay * 0.2 * Math.random(); // 20% jitter
const waitTime = delay + jitter;
console.log(
`[Retry] ${attempt + 1}/${maxRetries} ` +
`(after ${waitTime.toFixed(0)}ms): ${error.message}`
);
await new Promise((r) => setTimeout(r, waitTime));
}
}
throw lastError;
}
// Usage example
async function fetchDataWithRetry() {
const data = await withRetry(
async (attempt) => {
console.log(`Request attempt ${attempt + 1}`);
const response = await fetch("https://api.example.com/data");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
},
{
maxRetries: 3,
baseDelay: 1000,
// Do not retry on 4xx errors (client errors)
shouldRetry: (error) => !error.message.startsWith("HTTP 4"),
}
);
return data;
}
Timeout Pattern
A pattern that throws an error if a task does not complete within a specified time.
function withTimeout(promise, ms, message) {
/**
* Applies a timeout to a Promise.
* @param {Promise} promise - The Promise to apply a timeout to
* @param {number} ms - Timeout duration (milliseconds)
* @param {string} message - Timeout error message
*/
const timeout = new Promise((_, reject) => {
const timer = setTimeout(() => {
reject(new Error(message || `Timeout exceeded: ${ms}ms`));
}, ms);
// Clean up timer if the Promise completes first
promise.finally(() => clearTimeout(timer));
});
return Promise.race([promise, timeout]);
}
// Usage example
async function fetchWithTimeout() {
try {
const data = await withTimeout(
fetch("https://api.example.com/slow-endpoint"),
5000,
"API response timed out after 5 seconds"
);
return data.json();
} catch (error) {
console.error("Request failed:", error.message);
// API response timed out after 5 seconds
}
}
Composite Pattern — Retry + Timeout + Concurrency Limit
In practice, multiple patterns are combined together.
async function robustFetch(urls, options = {}) {
/**
* Fetches multiple URLs reliably.
* Applies retry, timeout, and concurrency limiting.
*/
const {
concurrency = 3,
timeout = 5000,
maxRetries = 2,
} = options;
const tasks = urls.map(
(url) => async () => {
return withRetry(
async () => {
const response = await withTimeout(
fetch(url),
timeout,
`${url} timed out`
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${url}`);
}
return { url, data: await response.json() };
},
{ maxRetries }
);
}
);
const results = await promisePool(tasks, concurrency);
const succeeded = results.filter((r) => !r?.error);
const failed = results.filter((r) => r?.error);
console.log(`Done: ${succeeded.length} succeeded, ${failed.length} failed`);
return { succeeded, failed };
}
// Usage example
// const urls = Array.from({ length: 50 }, (_, i) =>
// `https://api.example.com/items/${i}`
// );
// const { succeeded, failed } = await robustFetch(urls, {
// concurrency: 5,
// timeout: 3000,
// maxRetries: 2,
// });
Practical Tips
- Prefer
allSettled: For multiple independent requests, useallSettledinstead ofall. A single failure should not break everything. - Exponential backoff: Increase retry intervals exponentially and add jitter to prevent server overload from simultaneous retries.
- Cancellable tasks: Use
AbortControllerto cancel fetch requests. - Watch memory usage: Creating thousands of Promises simultaneously can spike memory. Always limit concurrency.
- Classify errors: Distinguish between retryable errors (5xx, network) and non-retryable errors (4xx, auth).
- Logging: Log retries and timeouts so issues can be traced.