Handling Multiple Async Operations in JavaScript
When building real-world applications, you often need to perform multiple asynchronous operations simultaneously. This article covers advanced techniques for managing concurrent async operations, coordinating multiple promises, and handling complex async workflows.
Introduction
Modern JavaScript applications frequently need to:
- Fetch data from multiple API endpoints
- Process multiple files or database queries
- Coordinate dependent and independent async tasks
- Handle partial failures in concurrent operations
- Implement timeout and cancellation patterns
Understanding how to efficiently handle multiple async operations is crucial for building performant, responsive applications.
Promise Combinators
Promise.all() - Wait for All Promises
Promise.all() waits for all promises to resolve or rejects if any promise rejects.
// Basic usage
const promise1 = fetch('/api/users');
const promise2 = fetch('/api/posts');
const promise3 = fetch('/api/comments');
Promise.all([promise1, promise2, promise3])
.then(responses => {
console.log('All requests completed');
return Promise.all(responses.map(r => r.json()));
})
.then(data => {
console.log('Users:', data[0]);
console.log('Posts:', data[1]);
console.log('Comments:', data[2]);
})
.catch(error => {
console.error('One or more requests failed:', error);
});
// With async/await
async function fetchAllData() {
try {
const [usersRes, postsRes, commentsRes] = await Promise.all([
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/comments')
]);
const [users, posts, comments] = await Promise.all([
usersRes.json(),
postsRes.json(),
commentsRes.json()
]);
return { users, posts, comments };
} catch (error) {
console.error('Failed to fetch data:', error);
throw error;
}
}
// Practical example: Parallel database queries
async function getUserProfile(userId) {
try {
const [user, posts, followers] = await Promise.all([
db.users.findById(userId),
db.posts.findByUserId(userId),
db.followers.findByUserId(userId)
]);
return {
user,
posts,
followers,
postCount: posts.length,
followerCount: followers.length
};
} catch (error) {
console.error('Error fetching user profile:', error);
throw error;
}
}
Key characteristics:
- Rejects immediately if any promise rejects
- Returns array in same order as input
- Efficient for independent operations
- All-or-nothing semantics
Promise.race() - First to Complete
Promise.race() resolves or rejects with the first promise to settle.
// Basic usage
const fetchPromise = fetch('/api/data');
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Request timeout')), 5000)
);
Promise.race([fetchPromise, timeoutPromise])
.then(response => response.json())
.then(data => console.log('Data:', data))
.catch(error => console.error('Error:', error));
// Implementing request timeout
function fetchWithTimeout(url, timeout = 5000) {
const fetchPromise = fetch(url);
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Request timeout after ${timeout}ms`)), timeout)
);
return Promise.race([fetchPromise, timeoutPromise]);
}
// Usage
fetchWithTimeout('/api/slow-endpoint', 3000)
.then(response => response.json())
.catch(error => console.error('Request failed:', error));
// Racing multiple data sources
async function fetchFromFastestSource(urls) {
const promises = urls.map(url => fetch(url).then(r => r.json()));
try {
const data = await Promise.race(promises);
console.log('Got data from fastest source:', data);
return data;
} catch (error) {
console.error('All sources failed:', error);
throw error;
}
}
// Usage
const data = await fetchFromFastestSource([
'https://api1.example.com/data',
'https://api2.example.com/data',
'https://api3.example.com/data'
]);
Key characteristics:
- Returns first settled promise
- Useful for timeouts and fallbacks
- Remaining promises continue executing
- First rejection also rejects
Promise.allSettled() - All Results
Promise.allSettled() waits for all promises to settle and returns their results.
// Basic usage
const promises = [
Promise.resolve('Success 1'),
Promise.reject(new Error('Failed')),
Promise.resolve('Success 2')
];
Promise.allSettled(promises)
.then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Promise ${index} resolved:`, result.value);
} else {
console.log(`Promise ${index} rejected:`, result.reason);
}
});
});
// Practical example: Batch API requests with partial failures
async function batchFetchUsers(userIds) {
const promises = userIds.map(id =>
fetch(`/api/users/${id}`).then(r => r.json())
);
const results = await Promise.allSettled(promises);
const successful = [];
const failed = [];
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
successful.push(result.value);
} else {
failed.push({
userId: userIds[index],
error: result.reason
});
}
});
return { successful, failed };
}
// Usage
const { successful, failed } = await batchFetchUsers([1, 2, 3, 4, 5]);
console.log(`Loaded ${successful.length} users, ${failed.length} failed`);
// Handling mixed success/failure scenarios
async function processMultipleFiles(files) {
const uploadPromises = files.map(file =>
uploadFile(file).catch(error => ({
file: file.name,
error: error.message
}))
);
const results = await Promise.allSettled(uploadPromises);
const uploads = {
successful: [],
failed: []
};
results.forEach(result => {
if (result.status === 'fulfilled') {
if (result.value.error) {
uploads.failed.push(result.value);
} else {
uploads.successful.push(result.value);
}
}
});
return uploads;
}
Key characteristics:
- Never rejects
- Returns array of status objects
- Useful for partial success scenarios
- All promises complete before returning
Promise.any() - First Success
Promise.any() resolves with the first promise to fulfill, or rejects if all reject.
// Basic usage
const promises = [
Promise.reject(new Error('Failed 1')),
Promise.reject(new Error('Failed 2')),
Promise.resolve('Success')
];
Promise.any(promises)
.then(value => console.log('First success:', value))
.catch(error => console.error('All failed:', error));
// Practical example: Fallback API endpoints
async function fetchDataWithFallback() {
const endpoints = [
'https://primary-api.example.com/data',
'https://backup-api.example.com/data',
'https://cache-api.example.com/data'
];
const promises = endpoints.map(url =>
fetch(url)
.then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
})
);
try {
const data = await Promise.any(promises);
console.log('Data from first successful endpoint:', data);
return data;
} catch (error) {
console.error('All endpoints failed:', error);
throw error;
}
}
// Racing multiple data sources with success priority
async function getDataFromAnySource(sources) {
const promises = sources.map(source =>
source.fetch()
.then(data => ({
source: source.name,
data,
timestamp: Date.now()
}))
);
try {
const result = await Promise.any(promises);
console.log(`Got data from ${result.source}`);
return result;
} catch (error) {
console.error('No sources available:', error);
throw error;
}
}
Key characteristics:
- Resolves with first fulfilled promise
- Rejects only if all promises reject
- Useful for fallback scenarios
- Ignores rejections until all fail
Sequential vs Concurrent Operations
Sequential Execution
// Sequential: Each operation waits for previous
async function sequentialFetch() {
const user = await fetch('/api/user').then(r => r.json());
const posts = await fetch(`/api/users/${user.id}/posts`).then(r => r.json());
const comments = await fetch(`/api/posts/${posts[0].id}/comments`).then(r => r.json());
return { user, posts, comments };
}
// Takes: ~3 seconds (if each request is 1 second)
Concurrent Execution
// Concurrent: Independent operations run in parallel
async function concurrentFetch() {
const [user, posts, comments] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json())
]);
return { user, posts, comments };
}
// Takes: ~1 second (all requests run in parallel)
// Mixed: Some sequential, some concurrent
async function mixedFetch() {
// First, get user (required for next step)
const user = await fetch('/api/user').then(r => r.json());
// Then, fetch user-specific data in parallel
const [posts, followers] = await Promise.all([
fetch(`/api/users/${user.id}/posts`).then(r => r.json()),
fetch(`/api/users/${user.id}/followers`).then(r => r.json())
]);
return { user, posts, followers };
}
Advanced Patterns
Retry with Exponential Backoff
async function retryWithBackoff(fn, maxRetries = 3, baseDelay = 1000) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries - 1) throw error;
const delay = baseDelay * Math.pow(2, attempt);
console.log(`Attempt ${attempt + 1} failed, retrying in ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Usage
const data = await retryWithBackoff(
() => fetch('/api/unreliable-endpoint').then(r => r.json()),
3,
1000
);
Timeout Wrapper
function withTimeout(promise, timeoutMs) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Operation timeout')), timeoutMs)
)
]);
}
// Usage
try {
const data = await withTimeout(
fetch('/api/data').then(r => r.json()),
5000
);
} catch (error) {
console.error('Request failed or timed out:', error);
}
Batch Processing with Concurrency Limit
async function batchWithLimit(items, fn, limit = 3) {
const results = [];
const executing = [];
for (const item of items) {
const promise = Promise.resolve().then(() => fn(item));
results.push(promise);
if (limit <= items.length) {
executing.push(
promise.then(() => executing.splice(executing.indexOf(promise), 1))
);
if (executing.length >= limit) {
await Promise.race(executing);
}
}
}
return Promise.all(results);
}
// Usage
const urls = ['url1', 'url2', 'url3', 'url4', 'url5'];
const results = await batchWithLimit(
urls,
url => fetch(url).then(r => r.json()),
2 // Process 2 at a time
);
Cancellation Pattern
class CancellablePromise {
constructor(fn) {
this.cancelled = false;
this.promise = new Promise((resolve, reject) => {
fn(
value => !this.cancelled && resolve(value),
error => !this.cancelled && reject(error)
);
});
}
cancel() {
this.cancelled = true;
}
then(onFulfilled, onRejected) {
return this.promise.then(onFulfilled, onRejected);
}
catch(onRejected) {
return this.promise.catch(onRejected);
}
}
// Usage
const cancellable = new CancellablePromise((resolve, reject) => {
const timeout = setTimeout(() => resolve('Done'), 5000);
this.cancel = () => {
clearTimeout(timeout);
reject(new Error('Cancelled'));
};
});
// Cancel after 2 seconds
setTimeout(() => cancellable.cancel(), 2000);
Practical Examples
Parallel Data Loading
async function loadDashboard(userId) {
try {
const [user, stats, notifications, settings] = await Promise.all([
fetchUser(userId),
fetchUserStats(userId),
fetchNotifications(userId),
fetchUserSettings(userId)
]);
return {
user,
stats,
notifications,
settings,
loadedAt: new Date()
};
} catch (error) {
console.error('Failed to load dashboard:', error);
throw error;
}
}
Fallback Chain
async function fetchWithFallbacks(primaryUrl, fallbackUrls = []) {
const allUrls = [primaryUrl, ...fallbackUrls];
const promises = allUrls.map(url =>
fetch(url)
.then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
})
.catch(error => {
console.warn(`Failed to fetch from ${url}:`, error.message);
throw error;
})
);
try {
return await Promise.any(promises);
} catch (error) {
console.error('All sources exhausted');
throw error;
}
}
Partial Success Handling
async function syncMultipleServices(data) {
const services = [
{ name: 'analytics', sync: () => syncAnalytics(data) },
{ name: 'cache', sync: () => syncCache(data) },
{ name: 'backup', sync: () => syncBackup(data) }
];
const results = await Promise.allSettled(
services.map(s => s.sync())
);
const report = {
successful: [],
failed: []
};
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
report.successful.push(services[index].name);
} else {
report.failed.push({
service: services[index].name,
error: result.reason.message
});
}
});
return report;
}
Best Practices
-
Choose the right combinator:
Promise.all()for all-or-nothingPromise.race()for timeoutsPromise.allSettled()for partial successPromise.any()for fallbacks
-
Avoid unnecessary nesting:
// โ Bad Promise.all([p1, p2]).then(results => Promise.all([p3, p4]).then(more => ...) ); // โ Good const [r1, r2] = await Promise.all([p1, p2]); const [r3, r4] = await Promise.all([p3, p4]); -
Handle errors appropriately:
// โ Good try { const results = await Promise.all(promises); } catch (error) { console.error('Operation failed:', error); } -
Limit concurrent operations:
- Prevent overwhelming servers
- Use concurrency limits for large batches
- Monitor resource usage
-
Use timeouts for external APIs:
const data = await withTimeout(fetchData(), 5000);
Common Mistakes
-
Forgetting Promise.all() for independent operations:
// โ Slow const a = await fetchA(); const b = await fetchB(); // โ Fast const [a, b] = await Promise.all([fetchA(), fetchB()]); -
Not handling partial failures:
// โ Fails completely await Promise.all(promises); // โ Handles partial failures const results = await Promise.allSettled(promises); -
Ignoring timeout scenarios:
// โ Can hang indefinitely await fetch(url); // โ Has timeout await withTimeout(fetch(url), 5000);
Summary
Handling multiple async operations efficiently is essential for modern JavaScript development. Key takeaways:
- Promise.all() for coordinating multiple independent operations
- Promise.race() for implementing timeouts and first-to-complete patterns
- Promise.allSettled() for handling partial success scenarios
- Promise.any() for fallback chains
- Use concurrency limits for large batches
- Always implement timeouts for external APIs
- Choose sequential vs concurrent based on dependencies
Related Resources
- Promise Combinators - MDN
- Promise.all() - MDN
- Promise.race() - MDN
- Promise.allSettled() - MDN
- Promise.any() - MDN
- Async/Await - MDN
Next Steps
- Explore Error Handling with Promises and Async/Await
- Learn about Event Loop and Microtasks
- Study Advanced Promise Patterns in Level 3
- Practice with real API integration projects
Comments