Lesson 3.3: Parallel Execution
Duration: 60 minutes
Learning Objectives
By the end of this lesson, you will be able to:
- Understand the difference between sequential and parallel execution
- Use
Promise.all()to run operations concurrently - Apply
Promise.allSettled()for fault-tolerant parallel execution - Implement
Promise.race()for timeout patterns - Choose the right approach for different scenarios
Introduction
In the previous lessons, we used await sequentially - waiting for one operation to complete before starting the next. This is necessary when operations depend on each other. But when operations are independent, running them in parallel can significantly improve performance.
Imagine you need to fetch a user's profile, posts, and notifications. These three requests do not depend on each other - why wait for one to finish before starting the next?
Sequential vs Parallel Execution
Sequential Execution (Slow)
async function loadDashboardSequential(): Promise<Dashboard> {
console.time('sequential');
const profile = await fetchProfile(); // 500ms
const posts = await fetchPosts(); // 300ms
const notifications = await fetchNotifications(); // 200ms
console.timeEnd('sequential'); // ~1000ms total
return { profile, posts, notifications };
}
Each operation waits for the previous one. Total time: 500 + 300 + 200 = 1000ms.
Parallel Execution (Fast)
async function loadDashboardParallel(): Promise<Dashboard> {
console.time('parallel');
const [profile, posts, notifications] = await Promise.all([
fetchProfile(), // 500ms ─┐
fetchPosts(), // 300ms ─┼─ All start at the same time
fetchNotifications(), // 200ms ─┘
]);
console.timeEnd('parallel'); // ~500ms total
return { profile, posts, notifications };
}
All operations start at the same time. Total time: max(500, 300, 200) = 500ms.
Promise.all() Deep Dive
Promise.all() takes an array of Promises and returns a single Promise that resolves when all input Promises resolve.
const results = await Promise.all([promise1, promise2, promise3]);
// results is an array: [result1, result2, result3]
Important Characteristics
- Order is preserved: Results are in the same order as input Promises
- Fail-fast: If any Promise rejects, the entire
Promise.all()rejects immediately - All or nothing: You either get all results or an error
Practical Example
interface User {
id: number;
name: string;
}
async function fetchUsers(ids: number[]): Promise<User[]> {
const promises = ids.map((id) => fetch(`/api/users/${id}`).then((r) => r.json()));
return await Promise.all(promises);
}
// Usage
const users = await fetchUsers([1, 2, 3, 4, 5]);
console.log(users); // Array of 5 users
Handling the Fail-Fast Behavior
The fail-fast behavior can be problematic. If one request fails, you lose all results.
async function fetchUsersRisky(ids: number[]): Promise<User[]> {
try {
return await Promise.all(ids.map((id) => fetchUser(id)));
} catch (error) {
// One failure = all results lost
console.error('At least one fetch failed:', error);
return [];
}
}
Promise.allSettled() for Fault Tolerance
Promise.allSettled() waits for all Promises to settle (either fulfill or reject) and never fails.
const results = await Promise.allSettled([
fetchUser(1),
fetchUser(2),
fetchUser(999), // This one fails
]);
// results is an array of objects:
// [
// { status: "fulfilled", value: { id: 1, name: "Alice" } },
// { status: "fulfilled", value: { id: 2, name: "Bob" } },
// { status: "rejected", reason: Error("User not found") }
// ]
Processing allSettled Results
interface SettledResult<T> {
status: 'fulfilled' | 'rejected';
value?: T;
reason?: unknown;
}
async function fetchUsersSafe(ids: number[]): Promise<User[]> {
const results = await Promise.allSettled(ids.map((id) => fetchUser(id)));
const users: User[] = [];
const errors: { id: number; error: unknown }[] = [];
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
users.push(result.value);
} else {
errors.push({ id: ids[index], error: result.reason });
}
});
if (errors.length > 0) {
console.warn(`Failed to fetch ${errors.length} users:`, errors);
}
return users;
}
Typed Helper for allSettled
function getSuccessful<T>(results: PromiseSettledResult<T>[]): T[] {
return results
.filter((r): r is PromiseFulfilledResult<T> => r.status === 'fulfilled')
.map((r) => r.value);
}
function getFailed<T>(results: PromiseSettledResult<T>[]): PromiseRejectedResult[] {
return results.filter((r): r is PromiseRejectedResult => r.status === 'rejected');
}
// Usage
const results = await Promise.allSettled(promises);
const successfulData = getSuccessful(results);
const failedReasons = getFailed(results).map((r) => r.reason);
Promise.race() for Timeouts
Promise.race() returns as soon as any Promise settles (either fulfills or rejects).
const result = await Promise.race([promise1, promise2, promise3]);
// result is the value of whichever Promise settled first
Timeout Pattern
The most common use of Promise.race() is implementing timeouts:
function timeout(ms: number): Promise<never> {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms);
});
}
async function fetchWithTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
return await Promise.race([promise, timeout(timeoutMs)]);
}
// Usage
try {
const data = await fetchWithTimeout(
fetch('/api/slow-endpoint').then((r) => r.json()),
5000 // 5 second timeout
);
console.log('Data received:', data);
} catch (error) {
console.error('Request timed out or failed:', error);
}
First Successful Response
Sometimes you want the first successful result (ignoring failures):
async function fetchFromFirstAvailable(urls: string[]): Promise<Response> {
return await Promise.any(urls.map((url) => fetch(url)));
}
// Falls back through mirrors
const response = await fetchFromFirstAvailable([
'https://primary.api.com/data',
'https://backup1.api.com/data',
'https://backup2.api.com/data',
]);
Promise.any() resolves with the first fulfilled Promise. It only rejects if all Promises reject.
Combining Sequential and Parallel
Real applications often need a mix of both approaches.
interface UserDashboard {
user: User;
posts: Post[];
recommendations: Recommendation[];
}
async function loadUserDashboard(userId: number): Promise<UserDashboard> {
// Step 1: We need the user first (required for other calls)
const user = await fetchUser(userId);
// Step 2: Fetch posts and recommendations in parallel
// (both depend on user, but not on each other)
const [posts, recommendations] = await Promise.all([
fetchUserPosts(user.id),
fetchRecommendations(user.preferences),
]);
return { user, posts, recommendations };
}
Diagram of Execution
Time ──────────────────────────────────────────────>
fetchUser() ████████████
│
├── fetchUserPosts() ██████████
│
└── fetchRecommendations() ████████
Sequential part Parallel part
Controlling Concurrency
Sometimes running too many operations in parallel causes problems (API rate limits, memory issues). You can limit concurrency.
Simple Batching
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((item) => processor(item)));
results.push(...batchResults);
}
return results;
}
// Process 100 items, 10 at a time
const userIds = Array.from({ length: 100 }, (_, i) => i + 1);
const users = await processBatch(userIds, fetchUser, 10);
Progress Tracking
async function processWithProgress<T, R>(
items: T[],
processor: (item: T) => Promise<R>,
onProgress: (completed: number, total: number) => void
): Promise<R[]> {
let completed = 0;
const total = items.length;
const results = await Promise.all(
items.map(async (item) => {
const result = await processor(item);
completed++;
onProgress(completed, total);
return result;
})
);
return results;
}
// Usage
await processWithProgress(userIds, fetchUser, (completed, total) => {
console.log(`Progress: ${completed}/${total} (${Math.round((completed / total) * 100)}%)`);
});
Exercise
Implement a function that loads a complete blog post page with the following requirements:
- Fetch the post by ID (required)
- Fetch the author details (requires post.authorId)
- Fetch comments and related posts in parallel (both require post.id)
- Use
Promise.allSettled()for comments and related posts so the page still loads if one fails
interface BlogPage {
post: Post;
author: Author;
comments: Comment[];
relatedPosts: Post[];
}
async function loadBlogPage(postId: number): Promise<BlogPage> {
// Your implementation
}
Solution:
async function loadBlogPage(postId: number): Promise<BlogPage> {
// Step 1: Fetch the post (required)
const post = await fetchPost(postId);
// Step 2: Fetch author (depends on post)
const author = await fetchAuthor(post.authorId);
// Step 3: Fetch comments and related posts in parallel with fault tolerance
const [commentsResult, relatedResult] = await Promise.allSettled([
fetchComments(post.id),
fetchRelatedPosts(post.id),
]);
const comments = commentsResult.status === 'fulfilled' ? commentsResult.value : [];
const relatedPosts = relatedResult.status === 'fulfilled' ? relatedResult.value : [];
// Log any failures
if (commentsResult.status === 'rejected') {
console.warn('Failed to load comments:', commentsResult.reason);
}
if (relatedResult.status === 'rejected') {
console.warn('Failed to load related posts:', relatedResult.reason);
}
return { post, author, comments, relatedPosts };
}
Key Takeaways
- Sequential awaits are for dependent operations; parallel for independent ones
- Promise.all() runs operations in parallel but fails fast on any rejection
- Promise.allSettled() always completes and tells you what succeeded/failed
- Promise.race() is great for timeouts and "first response wins" patterns
- Promise.any() gives you the first successful result
- Combine sequential and parallel execution based on your data dependencies
- Consider batching when you need to limit concurrency
Performance Comparison
| Approach | Use Case | Failure Behavior |
|---|---|---|
| Sequential await | Dependent operations | Stops at first error |
| Promise.all() | Independent, all required | Fails if any fails |
| Promise.allSettled() | Independent, partial OK | Never fails |
| Promise.race() | Timeouts, first wins | First to settle |
| Promise.any() | Fallbacks, mirrors | First success wins |
Resources
| Resource | Type | Level |
|---|---|---|
| MDN: Promise.all() | Documentation | Beginner |
| MDN: Promise.allSettled() | Documentation | Intermediate |
| MDN: Promise.race() | Documentation | Intermediate |
| MDN: Promise.any() | Documentation | Intermediate |
Next Lesson
Continue to Lesson 3.4: Typing Async Functions to learn how to properly type async functions in TypeScript.