Lesson 2.4: Promise.all and Promise.race
Duration: 50 minutes
Learning Objectives
By the end of this lesson, you will be able to:
- Run multiple Promises in parallel using
Promise.all() - Handle the first completed Promise with
Promise.race() - Choose the right method for different scenarios
- Understand
Promise.allSettled()andPromise.any() - Implement common patterns like timeouts and fallbacks
Why Combine Promises?
Often, you need to perform multiple async operations:
- Sequentially: One after another (use chaining)
- In Parallel: All at once, wait for all (use
Promise.all) - Racing: All at once, use the first result (use
Promise.race)
// Sequential - slow (3 seconds total)
const user = await fetchUser(); // 1 second
const posts = await fetchPosts(); // 1 second
const comments = await fetchComments(); // 1 second
// Parallel - fast (1 second total if independent)
const [user, posts, comments] = await Promise.all([
fetchUser(), // 1 second
fetchPosts(), // 1 second (runs simultaneously)
fetchComments(), // 1 second (runs simultaneously)
]);
Promise.all()
Promise.all() takes an array of Promises and returns a new Promise that:
- Resolves when ALL input Promises resolve
- Rejects immediately if ANY Promise rejects
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log(values); // [1, 2, 3]
});
Basic Example
function delay(ms: number, value: string): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => resolve(value), ms);
});
}
console.log('Starting...');
const startTime = Date.now();
Promise.all([delay(1000, 'First'), delay(2000, 'Second'), delay(1500, 'Third')]).then((results) => {
const elapsed = Date.now() - startTime;
console.log(`Results: ${results}`); // ["First", "Second", "Third"]
console.log(`Time: ${elapsed}ms`); // ~2000ms (not 4500ms!)
});
The total time is determined by the slowest Promise, not the sum.
Practical Example: Fetching Related Data
interface User {
id: number;
name: string;
}
interface UserProfile {
bio: string;
}
interface UserSettings {
theme: string;
notifications: boolean;
}
function fetchUser(id: number): Promise<User> {
return new Promise((resolve) => {
setTimeout(() => resolve({ id, name: 'Alice' }), 500);
});
}
function fetchProfile(userId: number): Promise<UserProfile> {
return new Promise((resolve) => {
setTimeout(() => resolve({ bio: 'TypeScript developer' }), 700);
});
}
function fetchSettings(userId: number): Promise<UserSettings> {
return new Promise((resolve) => {
setTimeout(() => resolve({ theme: 'dark', notifications: true }), 600);
});
}
// Fetch all user data in parallel
Promise.all([fetchUser(1), fetchProfile(1), fetchSettings(1)])
.then(([user, profile, settings]) => {
console.log('User:', user.name);
console.log('Bio:', profile.bio);
console.log('Theme:', settings.theme);
})
.catch((error) => {
console.error('Failed to load user data:', error.message);
});
Error Handling with Promise.all()
If any Promise rejects, the entire Promise.all() rejects immediately:
const promise1 = Promise.resolve('Success');
const promise2 = Promise.reject(new Error('Failed!'));
const promise3 = Promise.resolve('Also success');
Promise.all([promise1, promise2, promise3])
.then((values) => {
console.log('All resolved:', values); // Never runs
})
.catch((error) => {
console.log('One failed:', error.message); // "One failed: Failed!"
});
Important: The other Promises continue running, but their results are discarded.
TypeScript Typing
TypeScript infers the return type based on input Promises:
// TypeScript infers: Promise<[string, number, boolean]>
const result = Promise.all([Promise.resolve('hello'), Promise.resolve(42), Promise.resolve(true)]);
result.then(([str, num, bool]) => {
// str: string, num: number, bool: boolean
console.log(str.toUpperCase()); // TypeScript knows it's a string
});
Promise.race()
Promise.race() returns a Promise that settles as soon as any input Promise settles:
const fast = delay(100, 'Fast');
const slow = delay(500, 'Slow');
Promise.race([fast, slow]).then((winner) => {
console.log('Winner:', winner); // "Winner: Fast"
});
Racing with Errors
The first Promise to settle wins, whether it resolves or rejects:
const success = delay(200, 'Success');
const failure = new Promise<string>((_, reject) => {
setTimeout(() => reject(new Error('Failed')), 100);
});
Promise.race([success, failure])
.then((value) => console.log('Resolved:', value))
.catch((error) => console.log('Rejected:', error.message));
// Output: "Rejected: Failed" (failure completes first)
Practical Use Case: Timeout
function fetchWithTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
const timeout = new Promise<never>((_, reject) => {
setTimeout(() => {
reject(new Error(`Operation timed out after ${timeoutMs}ms`));
}, timeoutMs);
});
return Promise.race([promise, timeout]);
}
// Usage
const slowFetch = new Promise<string>((resolve) => {
setTimeout(() => resolve('Data'), 5000);
});
fetchWithTimeout(slowFetch, 2000)
.then((data) => console.log('Data:', data))
.catch((error) => console.log('Error:', error.message));
// Output: "Error: Operation timed out after 2000ms"
Practical Use Case: First Available Server
interface ServerResponse {
server: string;
data: string;
}
function fetchFromServer(server: string, delay: number): Promise<ServerResponse> {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ server, data: `Data from ${server}` });
}, delay);
});
}
// Race multiple servers, use the fastest
Promise.race([
fetchFromServer('US', 200),
fetchFromServer('EU', 150),
fetchFromServer('Asia', 300),
]).then((response) => {
console.log(`${response.server} responded first!`);
console.log('Data:', response.data);
});
// Output: "EU responded first!"
Promise.allSettled()
Promise.allSettled() waits for all Promises to settle (resolve or reject) and returns their results:
const promises = [
Promise.resolve('Success 1'),
Promise.reject(new Error('Error 2')),
Promise.resolve('Success 3'),
];
Promise.allSettled(promises).then((results) => {
console.log(results);
// [
// { status: "fulfilled", value: "Success 1" },
// { status: "rejected", reason: Error: Error 2 },
// { status: "fulfilled", value: "Success 3" }
// ]
});
When to Use allSettled()
Use it when you want all results, even if some fail:
interface FetchResult {
url: string;
data?: string;
error?: string;
}
async function fetchMultipleUrls(urls: string[]): Promise<FetchResult[]> {
const promises = urls.map((url) =>
fetch(url)
.then((response) => response.text())
.then((data) => ({ url, data }))
.catch((error) => ({ url, error: error.message }))
);
const results = await Promise.allSettled(promises);
return results.map((result, index) => {
if (result.status === 'fulfilled') {
return result.value;
} else {
return { url: urls[index], error: result.reason.message };
}
});
}
Filtering Results
interface User {
id: number;
name: string;
}
function fetchUser(id: number): Promise<User> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id % 2 === 0) {
resolve({ id, name: `User ${id}` });
} else {
reject(new Error(`User ${id} not found`));
}
}, 100);
});
}
const userIds = [1, 2, 3, 4, 5];
Promise.allSettled(userIds.map((id) => fetchUser(id))).then((results) => {
const successful = results
.filter((r): r is PromiseFulfilledResult<User> => r.status === 'fulfilled')
.map((r) => r.value);
const failed = results
.filter((r): r is PromiseRejectedResult => r.status === 'rejected')
.map((r) => r.reason.message);
console.log('Loaded users:', successful);
console.log('Failed:', failed);
});
Promise.any()
Promise.any() returns the first fulfilled Promise, ignoring rejections:
const promises = [
Promise.reject(new Error('Error 1')),
new Promise((resolve) => setTimeout(() => resolve('Success!'), 100)),
Promise.reject(new Error('Error 2')),
];
Promise.any(promises)
.then((value) => {
console.log('First success:', value); // "First success: Success!"
})
.catch((error) => {
// AggregateError if ALL promises reject
console.log('All failed:', error.errors);
});
Difference from Promise.race()
| Method | Returns | On First Reject |
|---|---|---|
race() |
First settled (success or failure) | Returns rejection |
any() |
First fulfilled only | Ignores, waits for success |
const promises = [
new Promise((_, reject) => setTimeout(() => reject(new Error('Fast fail')), 50)),
new Promise((resolve) => setTimeout(() => resolve('Slow success'), 200)),
];
// race() returns the fast failure
Promise.race(promises)
.then((v) => console.log('Race resolved:', v))
.catch((e) => console.log('Race rejected:', e.message));
// Output: "Race rejected: Fast fail"
// any() waits for success
Promise.any(promises)
.then((v) => console.log('Any resolved:', v))
.catch((e) => console.log('Any rejected:', e.message));
// Output: "Any resolved: Slow success"
Use Case: Multiple Fallback Sources
function fetchFromPrimary(): Promise<string> {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error('Primary unavailable')), 100);
});
}
function fetchFromBackup(): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => resolve('Data from backup'), 200);
});
}
function fetchFromCache(): Promise<string> {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error('Cache miss')), 50);
});
}
Promise.any([fetchFromPrimary(), fetchFromBackup(), fetchFromCache()])
.then((data) => {
console.log('Got data:', data); // "Got data: Data from backup"
})
.catch((error) => {
console.log('All sources failed:', error.errors);
});
Comparison Table
| Method | Resolves When | Rejects When |
|---|---|---|
Promise.all() |
ALL fulfill | ANY rejects |
Promise.race() |
FIRST settles | FIRST rejects |
Promise.allSettled() |
ALL settle | Never (always resolves) |
Promise.any() |
FIRST fulfills | ALL reject |
Practical Patterns
Pattern 1: Parallel with Timeout
async function fetchAllWithTimeout<T>(promises: Promise<T>[], timeoutMs: number): Promise<T[]> {
const timeout = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('Timeout')), timeoutMs);
});
return Promise.race([Promise.all(promises), timeout]);
}
// Usage
fetchAllWithTimeout([fetchUser(1), fetchProfile(1), fetchSettings(1)], 3000)
.then(([user, profile, settings]) => {
console.log('All data loaded');
})
.catch((error) => {
console.log('Failed or timed out:', error.message);
});
Pattern 2: Batch Processing
async function processBatch<T, R>(
items: T[],
processor: (item: T) => Promise<R>,
batchSize: number
): Promise<R[]> {
const results: R[] = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchResults = await Promise.all(batch.map(processor));
results.push(...batchResults);
console.log(`Processed ${Math.min(i + batchSize, items.length)}/${items.length}`);
}
return results;
}
// Usage
const userIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
processBatch(userIds, fetchUser, 3).then((users) => {
console.log('All users:', users.length);
});
Pattern 3: First Success with Fallback
async function fetchWithFallback<T>(
primary: () => Promise<T>,
fallback: () => Promise<T>
): Promise<T> {
try {
return await primary();
} catch {
console.log('Primary failed, trying fallback...');
return fallback();
}
}
// Or using Promise.any for parallel fallback
function fetchWithParallelFallback<T>(sources: Array<() => Promise<T>>): Promise<T> {
return Promise.any(sources.map((source) => source()));
}
Pattern 4: Load with Progress
interface ProgressUpdate {
completed: number;
total: number;
percentage: number;
}
async function loadWithProgress<T>(
promises: Promise<T>[],
onProgress: (update: ProgressUpdate) => void
): Promise<T[]> {
let completed = 0;
const total = promises.length;
const wrapped = promises.map((p) =>
p.then((result) => {
completed++;
onProgress({
completed,
total,
percentage: Math.round((completed / total) * 100),
});
return result;
})
);
return Promise.all(wrapped);
}
// Usage
loadWithProgress([fetchUser(1), fetchProfile(1), fetchSettings(1)], (progress) => {
console.log(`Loading: ${progress.percentage}%`);
}).then((results) => {
console.log('All loaded!');
});
Exercises
Exercise 1: Parallel Fetch
Fetch three pieces of data in parallel and log the total time:
function fetchA(): Promise<string> {
return new Promise((resolve) => setTimeout(() => resolve('A'), 1000));
}
function fetchB(): Promise<string> {
return new Promise((resolve) => setTimeout(() => resolve('B'), 1500));
}
function fetchC(): Promise<string> {
return new Promise((resolve) => setTimeout(() => resolve('C'), 800));
}
Solution
const start = Date.now();
Promise.all([fetchA(), fetchB(), fetchC()]).then(([a, b, c]) => {
const elapsed = Date.now() - start;
console.log('Results:', a, b, c); // "Results: A B C"
console.log(`Time: ${elapsed}ms`); // ~1500ms
});
Exercise 2: Implement Timeout
Create a function that races a Promise against a timeout:
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {}
// Test
const slow = new Promise<string>((resolve) => {
setTimeout(() => resolve('Done'), 5000);
});
withTimeout(slow, 1000)
.then((v) => console.log('Resolved:', v))
.catch((e) => console.log('Error:', e.message));
// Should print "Error: Timeout" after 1 second
Solution
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
const timeout = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('Timeout')), ms);
});
return Promise.race([promise, timeout]);
}
Exercise 3: Get All Results
Use Promise.allSettled() to fetch multiple users and separate successes from failures:
function fetchUser(id: number): Promise<{ id: number; name: string }> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id === 3) {
reject(new Error('User 3 not found'));
} else {
resolve({ id, name: `User ${id}` });
}
}, 100);
});
}
const ids = [1, 2, 3, 4, 5];
Solution
Promise.allSettled(ids.map((id) => fetchUser(id))).then((results) => {
const successful = results
.filter(
(r): r is PromiseFulfilledResult<{ id: number; name: string }> => r.status === 'fulfilled'
)
.map((r) => r.value);
const failed = results
.map((r, i) => (r.status === 'rejected' ? ids[i] : null))
.filter((id): id is number => id !== null);
console.log('Loaded:', successful);
console.log('Failed IDs:', failed);
});
// Output:
// Loaded: [{ id: 1, name: "User 1" }, { id: 2, name: "User 2" }, ...]
// Failed IDs: [3]
Exercise 4: First Available
Use Promise.any() to get data from the first available source:
function fetchFromServerA(): Promise<string> {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error('Server A down')), 100);
});
}
function fetchFromServerB(): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => resolve('Data from B'), 300);
});
}
function fetchFromServerC(): Promise<string> {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error('Server C down')), 200);
});
}
Solution
Promise.any([fetchFromServerA(), fetchFromServerB(), fetchFromServerC()])
.then((data) => {
console.log('Got data:', data); // "Got data: Data from B"
})
.catch((error) => {
console.log('All servers failed:', error.errors);
});
Key Takeaways
Promise.all(): Run in parallel, wait for ALL, fail on ANY errorPromise.race(): Run in parallel, return FIRST to settlePromise.allSettled(): Run in parallel, wait for ALL, never failsPromise.any(): Run in parallel, return FIRST success, fail only if ALL fail- Use parallel execution for independent operations to save time
- Use race for timeouts and first-available scenarios
- Use allSettled when you need all results regardless of errors
- Use any when you want the first success with automatic fallback
Resources
| Resource | Type | Description |
|---|---|---|
| MDN: Promise.all() | Documentation | Promise.all reference |
| MDN: Promise.race() | Documentation | Promise.race reference |
| MDN: Promise.allSettled() | Documentation | Promise.allSettled reference |
| MDN: Promise.any() | Documentation | Promise.any reference |
Next Lesson
Now let us put everything together in a practical exercise: building a Sequential Data Loader that fetches related data from an API.