Async/await is the most used JavaScript feature and the most misused one. The syntax is so clean it hides the asynchronous machinery underneath, leading to sequential execution where parallel was intended, uncaught promise rejections, and memory leaks. This guide covers every pattern you need and every pitfall you will encounter in production.
⚡ TL;DR: Always
await Promise.all()for independent operations, not sequential awaits. Always handle errors with try/catch. UseAbortControllerfor cancellable requests. NeverawaitinforEach— it does nothing useful. Never mix.then()andawaitin the same function.
The biggest mistake: sequential instead of parallel
// SLOW: sequential — 650ms total
async function getPageData(userId) {
const user = await fetchUser(userId); // 200ms
const posts = await fetchPosts(userId); // 300ms
const settings = await fetchSettings(userId); // 150ms
return { user, posts, settings };
}
// FAST: parallel — 300ms total (slowest request wins)
async function getPageData(userId) {
const [user, posts, settings] = await Promise.all([
fetchUser(userId),
fetchPosts(userId),
fetchSettings(userId),
]);
return { user, posts, settings };
}
Promise.all vs allSettled vs race vs any
// Promise.all — rejects if ANY fails (fail-fast)
const [a, b] = await Promise.all([fetchA(), fetchB()]);
// Promise.allSettled — waits for ALL, never rejects
const results = await Promise.allSettled([fetchA(), fetchB()]);
results.forEach(r => {
if (r.status === 'fulfilled') use(r.value);
if (r.status === 'rejected') log(r.reason);
});
// Use when: partial success is acceptable
// Promise.race — first to settle wins
const result = await Promise.race([
fetchData(),
new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), 5000))
]);
// Promise.any — first to FULFILL (ignores rejections)
const fastest = await Promise.any([serverA(), serverB(), serverC()]);
// Use when: multiple redundant sources, want fastest
Error handling — the right patterns
// WRONG: swallowed error
async function bad() {
const data = await riskyOperation(); // throws = unhandled rejection
return data;
}
// WRONG: mixing .then() and try/catch — confusing
async function alsobad() {
try {
return await fetch('/api').then(r => r.json());
} catch (e) {} // What does this catch? Unclear.
}
// RIGHT: consistent async/await with try/catch
async function good() {
try {
const res = await fetch('/api');
if (!res.ok) throw new Error('HTTP ' + res.status);
return await res.json();
} catch (err) {
logger.error('API failed', err);
throw err; // Re-throw unless you can recover
}
}
// Result pattern — no exceptions escaping
async function safe(id) {
try {
return { ok: true, data: await fetchData(id) };
} catch (err) {
return { ok: false, error: err.message };
}
}
await in forEach — the silent bug
const ids = [1, 2, 3, 4, 5];
// WRONG: forEach ignores the returned Promise
ids.forEach(async (id) => {
await processItem(id); // This await is ignored by forEach!
});
// Code continues before any processItem completes
// CORRECT: parallel execution
await Promise.all(ids.map(id => processItem(id)));
// CORRECT: sequential execution
for (const id of ids) {
await processItem(id);
}
// CORRECT: controlled concurrency (3 at a time)
async function batchProcess(ids, batchSize = 3) {
for (let i = 0; i < ids.length; i += batchSize) {
await Promise.all(ids.slice(i, i + batchSize).map(processItem));
}
}
AbortController — cancellable requests
class SearchComponent {
#controller = null;
async search(query) {
this.#controller?.abort(); // Cancel previous search
this.#controller = new AbortController();
const { signal } = this.#controller;
try {
const res = await fetch("/api/search?q=" + query, { signal });
return res.json();
} catch (err) {
if (err.name === 'AbortError') return null; // Cancelled — ignore
throw err;
}
}
}
// Timeout with AbortController:
async function fetchWithTimeout(url, ms = 5000) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), ms);
try {
return await fetch(url, { signal: controller.signal });
} finally {
clearTimeout(id);
}
}
Async iterators — stream large datasets
// Paginated API — O(1) memory regardless of total records
async function* fetchAllPages(endpoint) {
let page = 1;
while (true) {
const { data, hasMore } = await fetch(endpoint + "?page=" + page).then(r => r.json());
yield* data;
if (!hasMore) break;
page++;
}
}
for await (const user of fetchAllPages('/api/users')) {
await processUser(user);
}
Top 10 async/await mistakes to avoid
- ❌ Sequential awaits for independent operations — use
Promise.all - ❌ Missing error handling — unhandled rejection crashes Node.js
- ❌
awaitinforEach— usefor...oforPromise.all(array.map(...)) - ❌ Mixing
.then()andawait— pick one style per function - ❌ No
AbortControlleron user-triggered requests — race conditions and memory leaks - ❌ Returning a Promise without
awaitinsidetry/catch— catch won't fire - ❌ Creating unbounded Promises in loops — overwhelms APIs and memory
- ❌ Async constructors — use static async factory methods instead
- ❌
awaitin synchronous callbacks (sort comparators) — silently breaks ordering - ❌ Not handling partial failures in
Promise.all— useallSettledfor independent tasks
These async patterns connect directly to the Node.js event loop guide — understanding the event loop explains why sequential awaits are costly. For TypeScript async typing, see the TypeScript generics deep dive. External reference: MDN async function documentation.
Recommended Books
→ Designing Data-Intensive Applications — The essential deep-dive on distributed systems, databases, and production engineering at scale.
→ The Pragmatic Programmer — Timeless principles for writing better code, debugging smarter, and advancing as an engineer.
Affiliate links. We earn a small commission at no extra cost to you.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
