Skip to main content

Advanced Async Patterns

Promise.all()

Execute multiple promises in parallel and wait for all to complete.

Promise.all basic usage
const fetchUserData = (id) => {
return new Promise(resolve => {
setTimeout(() => resolve({ id, name: `User ${id}` }), 100);
});
};

const fetchUserPosts = (id) => {
return new Promise(resolve => {
setTimeout(() => resolve({ userId: id, posts: 3 }), 150);
});
};

// Fetch data for user 1
Promise.all([
fetchUserData(1),
fetchUserPosts(1)
]).then(([userData, postData]) => {
console.log('User:', userData);
console.log('Posts:', postData);
});
// Takes ~150ms (the longest promise)

Real-world example:

Promise.all with API calls
async function loadUserDashboard(userId) {
try {
const [user, posts, comments, notifications] = await Promise.all([
fetchUser(userId),
fetchPosts(userId),
fetchComments(userId),
fetchNotifications(userId)
]);

return {
user,
posts,
comments,
notifications
};
} catch (error) {
console.error('Failed to load dashboard:', error);
throw error;
}
}

Error handling:

Promise.all error handling
Promise.all([
fetchUser(1),
fetchPosts(1),
fetchInvalidData() // This will reject
]).catch(error => {
// If ANY promise rejects, the whole thing fails
console.error('One or more operations failed:', error);
});

// Note: If one fails, others continue running but result is rejected

Promise.race()

Complete when the first promise settles (resolves or rejects).

Promise.race basic usage
const fetchWithTimeout = (url, timeout = 5000) => {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Request timeout')), timeout);
});

return Promise.race([
fetch(url),
timeoutPromise
]);
};

// Usage
fetchWithTimeout('https://api.example.com/data', 3000)
.then(response => response.json())
.catch(error => console.error('Request failed:', error.message));

Practical example:

Promise.race for fallback APIs
const fetchFromMultipleSources = async () => {
try {
return await Promise.race([
fetch('https://primary-api.com/data'),
fetch('https://backup-api-1.com/data'),
fetch('https://backup-api-2.com/data')
]);
} catch (error) {
console.error('All sources failed');
}
};

Promise.allSettled()

Wait for all promises to settle (resolve OR reject) and get all results.

Promise.allSettled basic usage
const promises = [
Promise.resolve('Success 1'),
Promise.reject('Error 1'),
Promise.resolve('Success 2'),
Promise.reject('Error 2')
];

Promise.allSettled(promises).then(results => {
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('✓ Success:', result.value);
} else {
console.log('✗ Failed:', result.reason);
}
});
});

Real-world example:

allSettled for batch operations
async function sendNotificationsToUsers(userIds) {
const promises = userIds.map(id =>
sendNotification(id).catch(error => ({
userId: id,
error: error.message
}))
);

const results = await Promise.allSettled(promises);

const successful = results.filter(r => r.status === 'fulfilled');
const failed = results.filter(r => r.status === 'rejected');

console.log(`Sent ${successful.length} notifications, ${failed.length} failed`);

return {
successful: successful.map(r => r.value),
failed: failed.map(r => r.reason)
};
}

Promise.any()

Complete when the first promise resolves (ignores rejections).

Promise.any basic usage
Promise.any([
Promise.reject('Error 1'),
Promise.reject('Error 2'),
Promise.resolve('Success!')
]).then(result => {
console.log(result); // 'Success!'
}).catch(error => {
// AggregateError if ALL reject
console.log('All promises rejected');
});

Concurrent Operations with Limits

Control concurrency when dealing with many operations:

Concurrency limiter
class ConcurrencyLimiter {
constructor(limit) {
this.limit = limit;
this.running = 0;
this.queue = [];
}

async run(fn) {
while (this.running >= this.limit) {
await new Promise(resolve => this.queue.push(resolve));
}

this.running++;

try {
return await fn();
} finally {
this.running--;
const resolve = this.queue.shift();
if (resolve) resolve();
}
}
}

// Usage
const limiter = new ConcurrencyLimiter(3); // Max 3 concurrent

const tasks = Array.from({ length: 10 }, (_, i) =>
limiter.run(() => fetchData(i))
);

await Promise.all(tasks);

Async Generator Functions

Combine async and generator patterns:

Async generator
async function* fetchDataInBatches(items, batchSize = 5) {
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const results = await Promise.all(
batch.map(item => fetchData(item))
);
yield results;
}
}

// Usage
(async () => {
for await (const batch of fetchDataInBatches(itemIds, 10)) {
console.log('Processing batch:', batch);
}
})();

Retry Logic

Automatically retry failed operations:

Retry with exponential backoff
async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 100) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries) throw error;

const delay = initialDelay * Math.pow(2, attempt - 1);
console.log(`Attempt ${attempt} failed. Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}

// Usage
const data = await retryWithBackoff(
() => fetch('https://api.example.com/data').then(r => r.json()),
3,
100
);

Debouncing and Throttling Async Functions

Async debounce
function debounceAsync(fn, delay) {
let timeoutId;
let lastPromise = null;

return async function debounced(...args) {
clearTimeout(timeoutId);

return new Promise((resolve, reject) => {
timeoutId = setTimeout(async () => {
try {
const result = await fn(...args);
resolve(result);
} catch (error) {
reject(error);
}
}, delay);
});
};
}

// Usage: Debounced API search
const debouncedSearch = debounceAsync(
async (query) => {
const response = await fetch(`/api/search?q=${query}`);
return response.json();
},
300
);

input.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});

Abort Controller for Cancellation

Cancel ongoing operations:

AbortController for fetch
const controller = new AbortController();

// Start a long request
const fetchPromise = fetch('https://slow-api.example.com/data', {
signal: controller.signal
});

// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);

try {
const response = await fetchPromise;
const data = await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request was cancelled');
}
}

Promise Chain vs Async/Await

Understanding the differences:

Promise chain approach
function loadUserWithPosts(userId) {
return fetchUser(userId)
.then(user =>
fetchPosts(user.id).then(posts => ({
...user,
posts
}))
)
.catch(error => console.error('Error:', error));
}
Async/await approach (cleaner)
async function loadUserWithPosts(userId) {
try {
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id);
return {
...user,
posts
};
} catch (error) {
console.error('Error:', error);
}
}

Practical Real-World Example

Complete data loading workflow
async function loadDashboardData(userId) {
const startTime = Date.now();
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 30000); // 30s timeout

try {
// Load critical data in parallel
const [user, stats] = await Promise.all([
retryWithBackoff(() => fetchUser(userId)),
retryWithBackoff(() => fetchStats(userId))
]);

// Load additional data
const [posts, comments, followers] = await Promise.allSettled([
fetchPosts(userId, { signal: controller.signal }),
fetchComments(userId, { signal: controller.signal }),
fetchFollowers(userId, { signal: controller.signal })
]);

// Process results
const dashboard = {
user,
stats,
posts: posts.status === 'fulfilled' ? posts.value : null,
comments: comments.status === 'fulfilled' ? comments.value : null,
followers: followers.status === 'fulfilled' ? followers.value : null
};

console.log(`Dashboard loaded in ${Date.now() - startTime}ms`);
return dashboard;
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Dashboard load timeout');
}
throw error;
} finally {
clearTimeout(timeout);
}
}

Performance Comparison

MethodUse CasePerformance
Promise.all()All succeed or fail togetherFastest
Promise.race()First to finish mattersFast
Promise.allSettled()Need all results regardlessMedium
Promise.any()One success is enoughMedium
Sequential awaitsDepend on previous resultsSlowest

Key Takeaways

  • Promise.all(): Wait for all promises, fail if any reject
  • Promise.race(): Use first to finish (great for timeouts)
  • Promise.allSettled(): Get all results, even failures
  • Promise.any(): Use first success, ignore rejections
  • AbortController: Cancel long-running operations
  • Retry logic: Handle transient failures gracefully
  • Async generators: Stream large datasets efficiently