TL;DR
- Async functions return immediately with a Promise (JS) or Task (Python
asyncio).awaitpauses that function only and schedules the rest to run when the awaited operation completes. - The event loop runs work in turns. I/O waits don’t block the loop; CPU work does — use workers/threads/processes for heavy CPU tasks.
- For concurrency, start work first and then await together (
Promise.all,asyncio.gather). Avoid serialawaitinside tight loops. - Always add timeouts/cancellation and handle errors. In JS:
AbortController,Promise.race. In Python:asyncio.timeout,Task.cancel(). - Prefer async iterators/streams and backpressure-friendly patterns for large data.
The mental model (60 seconds)
Event loop steps through a queue of tasks. When an async operation (network, file, timer) completes, a callback is queued. In modern code we write that callback as the continuation after await:
call stack microtasks (promises) tasks (timers, I/O)
---------- ---------------------- ---------------------
async fn ─ await ──► yields to loop ──► later, continuation runs
- In JavaScript,
async function→ returns a Promise immediately.await pdesugars to “pause this function; whenpsettles, resume with its value/error (microtask queue).” - In Python
asyncio,async defreturns a coroutine; the loop schedules it as a Task.awaityields control to the loop so other tasks can run.
Async ≠ parallel: You get concurrency on one thread when operations frequently wait on I/O. For true CPU parallelism, use workers/threads/processes.
Sync vs Async vs Concurrent vs Parallel
- Synchronous: do A, then B, then C — each must finish before the next starts.
- Asynchronous: start A; while A is waiting, you can do B.
- Concurrent: multiple things in progress at the same time (interleaved).
- Parallel: multiple things executing simultaneously on different cores.
JavaScript patterns
Sequential (slow)
async function getTwo() {
const a = await fetch("/api/a").then(r => r.json());
const b = await fetch("/api/b").then(r => r.json());
return [a, b]; // total time ~ a + b
}
Concurrent (better)
async function getTwo() {
const pa = fetch("/api/a").then(r => r.json());
const pb = fetch("/api/b").then(r => r.json());
return Promise.all([pa, pb]); // time ~ max(a, b)
}
Avoid await in a plain for loop
// ❌ does 100 requests one-by-one
for (const id of ids) {
const data = await fetch(`/api/items/${id}`);
}
Use map + Promise.all with optional concurrency limits:
const tasks = ids.map(id => () => fetch(`/api/items/${id}`).then(r => r.json()));
// simple: fire all at once (careful with 1000s)
const results = await Promise.all(tasks.map(fn => fn()));
// capped concurrency (tiny helper)
async function pool(fns, n){
const ret = []; const p = new Set();
for (const fn of fns){
const pr = fn().then(v => { ret.push(v); p.delete(pr); });
p.add(pr); if (p.size >= n) await Promise.race(p);
}
await Promise.all(p); return ret;
}
const resultsCapped = await pool(tasks, 5);
Timeouts & cancellation
// AbortController
const ac = new AbortController();
const timer = setTimeout(() => ac.abort(), 5000);
try {
const res = await fetch("/api/slow", { signal: ac.signal });
const data = await res.json();
} finally {
clearTimeout(timer);
}
Error handling
try {
const [a, b] = await Promise.all([getA(), getB()]);
} catch (err) {
// any failure in the group lands here
}
// capture all results (no short-circuit)
const results = await Promise.allSettled([getA(), getB()]);
Microtasks vs tasks (render timing)
console.log("A");
queueMicrotask(() => console.log("microtask"));
setTimeout(() => console.log("timeout 0"), 0);
console.log("B");
// order: A, B, microtask, timeout 0
Python asyncio patterns
Start first, await together
import asyncio, aiohttp
async def fetch(session, url):
async with session.get(url) as r:
return await r.text()
async def main(urls):
async with aiohttp.ClientSession() as session:
tasks = [asyncio.create_task(fetch(session, u)) for u in urls]
return await asyncio.gather(*tasks) # waits for all
asyncio.run(main(["/a", "/b", "/c"]))
Timeouts & cancellation
import asyncio
async def work():
await asyncio.sleep(10)
async def main():
try:
async with asyncio.timeout(5):
await work()
except TimeoutError:
print("timed out")
asyncio.run(main())
Cancel outstanding tasks:
task = asyncio.create_task(work())
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
Mixing CPU work (don’t block the loop)
import asyncio
def cpu_heavy(n): # pure CPU
s = 0
for i in range(n): s += i*i
return s
# run in a thread to keep event loop responsive
res = await asyncio.to_thread(cpu_heavy, 10_000_000)
Streams & backpressure
- JS:
ReadableStream/ Node streams support async iteration:
for await (const chunk of stream) {
// process as it arrives; apply backpressure naturally
}
- Python: many libs expose async iterables (
async for); process incrementally to avoid OOM.
Common pitfalls & fixes
| Problem | Why it hurts | Fix |
|---|---|---|
| await inside tight for loops | Serializes work | Start tasks first; Promise.all / asyncio.gather |
| Blocking CPU on the main loop | UI/loop freezes, timeouts | Offload to workers/threads/processes |
| No timeout/cancellation | Hangs forever | Use AbortController / asyncio.timeout and cancel |
| Swallowing errors in Promise.all | Hard to debug | Use allSettled or handle per‑task |
| Too many concurrent requests | rate limits, memory spikes | Use a pool / limit concurrency |
| Forgetting return in async | Returns undefined wrapped | Always return a value/promise |
Quick checklist
- [ ] Start work, then await together for concurrency.
- [ ] Add timeouts and support cancellation.
- [ ] Keep the loop non‑blocking; offload CPU.
- [ ] Prefer async iterators/streams for large data.
- [ ] Handle errors explicitly (group vs per‑task).
- [ ] Limit concurrency when hitting external services.
One‑minute adoption plan
- Identify I/O hotspots (HTTP, DB, filesystem).
- Refactor to
async/await; group awaits withPromise.all/gather. - Add timeouts + cancellation paths.
- Add a concurrency limiter where needed.
- Watch p95 latency, event loop lag, and error rates; move CPU work off the loop.