JavaScript Async/Await Cheat Sheet
Covers async function syntax, error handling with try/catch, and running promises sequentially versus concurrently with Promise combinators.
Basic async/await
async functions always return a Promise.
async function fetchUser(id) { const response = await fetch(`/api/users/${id}`); const data = await response.json(); return data;}// An async function always returns a PromisefetchUser(1).then(user => console.log(user));// Equivalent, inside another async functionasync function main() { const user = await fetchUser(1); console.log(user);}
Error Handling
Wrap awaited calls in try/catch/finally.
async function loadUser(id) { try { const res = await fetch(`/api/users/${id}`); if (!res.ok) { throw new Error(`HTTP ${res.status}`); } return await res.json(); } catch (err) { console.error("Failed to load user:", err.message); return null; } finally { console.log("request finished"); }}
Sequential vs Parallel
Awaiting in sequence pays the latency cost multiple times.
// Sequential -- each await blocks the next (slower, ~2x time)async function sequential() { const a = await fetchUser(1); const b = await fetchUser(2); return [a, b];}// Parallel -- both requests start immediatelyasync function parallel() { const [a, b] = await Promise.all([fetchUser(1), fetchUser(2)]); return [a, b];}// Parallel, tolerant of individual failuresasync function parallelSettled() { const results = await Promise.allSettled([fetchUser(1), fetchUser(2)]); return results.filter(r => r.status === "fulfilled").map(r => r.value);}
Iteration & Top-Level Await
Looping with await and module-level await.
async function processAll(ids) { const results = []; for (const id of ids) { results.push(await fetchUser(id)); // Runs one at a time } return results;}// Top-level await (in ES modules only)const config = await fetch("/config.json").then(r => r.json());
Promise Combinators
Ways to combine multiple promises.
- Promise.all()- Waits for all promises; rejects immediately if any one rejects
- Promise.allSettled()- Waits for all promises; never short-circuits, returns a status per item
- Promise.race()- Resolves/rejects as soon as the first promise settles
- Promise.any()- Resolves with the first fulfilled promise; rejects only if all reject
- await- Pauses the async function until the promise settles, unwrapping its value
- async function- Always returns a Promise, even if the body has no explicit await
Async Generators & for-await-of
Consume asynchronous streams of values lazily with async iteration.
async function* paginate(url) { let next = url; while (next) { const res = await fetch(next); const page = await res.json(); yield* page.items; // Flatten each page's items into the stream next = page.nextUrl; // null/undefined ends the loop }}async function consume() { for await (const item of paginate("/api/items?page=1")) { console.log(item.id); if (item.id === "stop-early") break; // return() is called on the generator }}// Adapting a callback-based/event stream into an async iterablefunction fromEmitter(emitter, event) { const queue = []; let resolveNext; emitter.on(event, (val) => { if (resolveNext) { resolveNext({ value: val, done: false }); resolveNext = null; } else queue.push(val); }); return { [Symbol.asyncIterator]() { return { next() { if (queue.length) return Promise.resolve({ value: queue.shift(), done: false }); return new Promise((resolve) => { resolveNext = resolve; }); }, }; }, };}
Cancellation with AbortController
Abort in-flight awaits deterministically instead of leaking pending promises.
async function fetchWithTimeout(url, ms) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(new Error("timeout")), ms); try { const res = await fetch(url, { signal: controller.signal }); return await res.json(); } catch (err) { if (err.name === "AbortError") throw new Error(`Request to ${url} timed out after ${ms}ms`); throw err; } finally { clearTimeout(timer); }}// Racing a promise against an abort signal for non-fetch async workfunction abortable(promise, signal) { return new Promise((resolve, reject) => { if (signal.aborted) return reject(new Error("already aborted")); signal.addEventListener("abort", () => reject(new Error("aborted")), { once: true }); promise.then(resolve, reject); });}
Bounded Concurrency (Worker Pool)
Promise.all fires everything at once -- throttle when hitting rate limits or connection caps.
async function mapWithConcurrency(items, limit, worker) { const results = new Array(items.length); let cursor = 0; async function runNext() { while (cursor < items.length) { const i = cursor++; results[i] = await worker(items[i], i); } } const pool = Array.from({ length: Math.min(limit, items.length) }, runNext); await Promise.all(pool); return results;}// Only 4 requests in flight at any time, regardless of items.lengthconst pages = await mapWithConcurrency(urls, 4, (url) => fetch(url).then((r) => r.json()));
Microtask Queue Gotchas
await yields to the microtask queue, not the macrotask queue -- ordering surprises setTimeout.
console.log("1: sync start");setTimeout(() => console.log("2: macrotask (setTimeout)"), 0);Promise.resolve().then(() => console.log("3: microtask"));async function demo() { console.log("4: sync inside async, runs immediately"); await null; // Suspends here, schedules a microtask to resume console.log("5: after await, microtask");}demo();console.log("6: sync end");// Order: 1, 4, 6, 3, 5, 2// All microtasks (promise callbacks, await resumptions) drain before the next macrotask
Advanced Pitfalls & Idioms
Mistakes that only show up once you're past the basics.
- Unhandled rejection in fire-and-forget- Calling an async function without await or .catch() and letting it reject crashes Node processes by default; always attach a handler or void it explicitly
- return await in try/catch- Inside a try block, `return await promise` (not `return promise`) is required so the catch block can actually intercept the rejection
- async executor anti-pattern- Never pass an async function as the Promise constructor executor; thrown errors inside it become unhandled rejections instead of constructor errors
- forEach with async callbacks- Array.prototype.forEach ignores returned promises entirely -- it doesn't await each iteration, so use for...of or Promise.all(items.map(...)) instead
- Zombie awaits after abort- An awaited promise that never settles (e.g. a dropped WebSocket response) leaks the entire async function's stack frame until GC; pair with AbortController or a timeout race
- Promise.any() AggregateError- When every input promise rejects, Promise.any() rejects with an AggregateError whose .errors array holds each individual reason
Awaiting independent promises one-by-one inside a loop serializes requests unnecessarily — start them all first (e.g. with map + Promise.all) so they run concurrently instead of paying the latency cost N times.