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 — JavaScript async await
// 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 fastestError 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);
}What the runtime is doing: microtasks, macrotasks and await
An async function returns a Promise immediately. Each await suspends that function and schedules the rest of it as a promise reaction, a microtask, once the awaited value settles. The thread itself isn't blocked: the event loop goes on running other code. Timers and I/O callbacks are macrotasks, and the microtask queue is drained completely before the event loop moves on to the next one. That's why a resolved promise's callback runs before setTimeout(fn, 0).
console.log("1 sync");
setTimeout(() => console.log("6 timer (macrotask)"), 0);
Promise.resolve().then(() => console.log("3 microtask"));
Promise.resolve()
.then(() => console.log("4 microtask"))
.then(() => console.log("5 chained microtask"));
console.log("2 sync");
// A loop that only awaits resolved promises never lets timers or I/O run:
// while (true) await Promise.resolve();
// Yield to the event loop between chunks of work instead.
const yieldToEventLoop = () => new Promise((resolve) => setImmediate(resolve));
async function processAll(items, handle, chunk = 1000) {
for (let i = 0; i < items.length; i++) {
handle(items[i]);
if (i % chunk === chunk - 1) await yieldToEventLoop();
}
}
let sum = 0;
await processAll(Array.from({ length: 5000 }, (_, i) => i), (x) => { sum += x; });
console.log("7 processed, sum =", sum);Run with Node.js, it prints 1 through 7 in order. The two synchronous logs run first, then all three microtasks (including the chained one, which is queued while the queue is draining), and only then the timer.
The same rule explains microtask starvation. A loop that only awaits already-resolved promises keeps adding microtasks, so timers, I/O and incoming requests never get a turn. For long CPU-bound loops, yield to the event loop now and then, as processAll does with setImmediate in Node.js (use setTimeout or scheduler.yield() where available in browsers), or move the work to a worker thread. See MDN's microtask guide and the Node.js event loop guide.
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.
Free Weekly Newsletter
🚀 Don’t Miss the Next Cheat Code
You just read something most developers never learn. Get more secrets like this delivered every week — JavaScript internals, Python optimizations, AWS architectures, system design, and AI workflows.
Related: Understanding Async/Await Under the Hood: The JavaScript Runtime Explained · TypeScript for JavaScript Developers: The Migration Guide That Actually Works · JavaScript Closures, Scope, and the Module Pattern: The Complete Guide
Related: JavaScript Promises: The Complete Guide From Basics to Advanced Patterns · JavaScript Array Methods: Every Method Explained With Real Examples · React Performance Optimization: Every Technique From memo to Virtualization
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- REST API Design Best Practices: The Patterns That Make APIs a Joy to Use
Newly added
- AWS Systems Manager Parameter Store: Hierarchical Runtime Config for Coding Agents
- Amazon Cognito Identity Pools: Federate Coding-Agent Tool Callers Without Long-Lived Keys
- Amazon Athena for Coding Agents: Query Tool Audit Logs and Eval Traces on S3 Without a Warehouse
- AWS Batch for Coding Agents: Parallel Overnight PR Reviews and Long-Running Jobs Without Lambda Timeouts
- Amazon Bedrock Prompt Management: Version and A/B Test Coding-Agent System Prompts
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
Already subscribed? or open the subscribe page.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
