concept · weight 7
Asynchronous Programming
Non-blocking concurrency built on event loops, futures, promises, and coroutines that lets a single thread overlap many in-flight I/O operations.
Asynchronous Programming
Definition
Asynchronous programming is a style of writing code where operations that wait — network calls, disk reads, timers, IPC — do not block the thread that issued them. Instead, the runtime suspends the waiting task, runs other work, and resumes the task when its result is ready. In modern languages it is expressed with coroutines marked async and a await (or equivalent) keyword that yields control to an event loop until a future / promise settles.
Why it matters
A server that serves one request at a time per OS thread runs into two ceilings fast: thread memory (often 1–8 MB of stack each) and kernel-scheduler overhead. Async lets a single OS thread juggle thousands of concurrent in-flight requests, because each suspended coroutine costs only a few hundred bytes of state and switching between them never crosses into the kernel. Real-time UIs, chat backends, scrapers, web crawlers, RPC fan-out, and any "wait on many slow things at once" workload benefit massively.
It is also a correctness tool: marking I/O await-points makes concurrency visible in the source. Reads pause at the keyword; nothing pre-empts mid-statement; data races between coroutines are impossible without a deliberate await. That sharply narrows the surface area you have to reason about compared to free-threaded code.
How it works
Three primitives form the substrate:
- The event loop — a single thread running an infinite loop that pulls ready callbacks off a queue, runs them to their next suspension point, and registers I/O wakeups with the OS (
epoll,kqueue,IOCP). When a coroutine yields, the loop is free to run any other ready coroutine. - Futures / Promises — objects that represent a value that will exist. A future starts pending and settles exactly once into fulfilled (with a value) or rejected (with an error). The loop wakes the awaiters when the state changes.
- Coroutines — stackful or stackless functions that can be paused at explicit yield points.
async fn() { await x }compiles into a state machine the loop drives one step at a time.
The runtime contract is cooperative multitasking: a coroutine only ever yields at an await. Between awaits it runs to completion. That is what makes shared state safe — and it is also the single biggest pitfall, because a CPU-bound loop with no awaits starves the entire process.
Composition primitives let you express joint waits cleanly:
- JavaScript —
Promise.all([a, b])resolves when both succeed, rejects on the first failure.Promise.allSettledreturns every outcome regardless.Promise.racesettles with whichever finishes first (success or failure).Promise.anyisracefor successes — it rejects only if every input rejects. - Python —
asyncio.gather(*tasks)isPromise.all-shaped.asyncio.waitis the lower-level building block.asyncio.TaskGroup(3.11+) and Trio/AnyIO nurseries add structured concurrency: every spawned task is bound to the lexical scope of the group, and the group cannot exit until all children have finished or one has crashed (taking siblings with it via cancellation).
Brief lineage: C# 5 (2012) shipped async/await first, popularizing the keyword pair. Python adopted it in 3.5 via PEP 492 (2015) on top of an event loop already exposed as asyncio. JavaScript landed it in ES2017, sugaring the Promise machinery added in ES2015. Structured concurrency was pioneered by Nathaniel J. Smith's Trio library, then formalized in asyncio.TaskGroup and made ergonomic by PEP 654's ExceptionGroup / except* syntax (Python 3.11).
Common pitfalls
- Forgotten
await— calling an async function without awaiting it returns the coroutine/promise object, not the value. Linters (@typescript-eslint/no-floating-promises,ruff'sRUF006) catch most cases; treat them as errors. - Blocking the event loop — calling
time.sleep, a syncrequests.get, a CPU-bound NumPy routine, orJSON.parseon a 50 MB string from inside a coroutine freezes every concurrent task. Useasyncio.to_thread/loop.run_in_executor(Python),worker_threads(Node), or just declare the routedefso the framework runs it in a thread pool (FastAPI does this automatically fordefroutes). Promise.allshort-circuits on rejection — one failure rejects the aggregate while sibling promises keep running in the background, often unhandled. UsePromise.allSettledwhen partial results are acceptable, or wrap each task in a.catch.- No cancellation discipline — without structured concurrency, a
for awaitthat throws mid-loop leaves background tasks orphaned. UseTaskGroup/ Trio nurseries /AbortControllerso cancellation propagates to siblings. - GIL still applies in Python —
asynciodoes not sidestep the Global Interpreter Lock. It overlaps I/O wait, not CPU work. For CPU-bound parallelism usemultiprocessing,concurrent.futures.ProcessPoolExecutor, or the no-GIL build (PEP 703). - "Async coloring" —
asyncis contagious: anasynccallee forces every caller up the chain to also beasync(or block withasyncio.run/.then). Decide early which layers are async and resist sprinkling it mid-codebase. AsyncClientlifetime leaks — HTTP clients (httpx.AsyncClient,aiohttp.ClientSession,node-fetch's keep-alive agent) hold connection pools. Build once per app, not per request, and close them withasync withor an explicitawait client.aclose().
Where to go next
- /sections/python/asyncio — Python's event loop,
gather,TaskGroup, timeouts, queues, and the blocking-call trap. - /sections/javascript/async-await — JS sugar over Promises, parallel vs sequential loops, top-level await,
AbortController. - /sections/javascript/promises — the underlying state machine,
thenchains, combinators, anti-patterns. - /sections/python/httpx — sync vs async HTTP client surfaces sharing one connection pool.
- /sections/python/fastapi — async-first web framework; the canonical place where the blocking-route pitfall bites.
- /sections/claude-api/streaming —
AsyncAnthropicandasync forover SSE. - /sections/claude-api/batch-api — the other shape of async: fire-and-poll for bulk work.
- #event-loop — articles across the knowledge base that drill into the scheduler itself.
- #concurrency — the broader landscape: threads, processes, actors, CSP.
Sources
References consulted while writing this concept page. Links open in a new tab.
- PEP 492 — Coroutines with async and await syntax — Yury Selivanov's 2015 proposal that introduced Python's
async def/awaitkeywords on top ofasyncio. - Wikipedia: Async/await — Cross-language lineage from C# 5 (2012) through F#, Python 3.5, ES2017, Rust, Swift, and Kotlin.
- Trio core reference — nurseries — The structured-concurrency primitive that inspired
asyncio.TaskGroupand the AnyIO API. - PEP 654 — Exception Groups and except* — The language-level support that made
TaskGroupergonomic by letting one block handle multiple concurrent failures. - Cloudflare — The problem with event loops — Grounded the "blocking the event loop starves every task" pitfall and the cost model for cooperative schedulers.
- MDN — Promise — Canonical reference for
Promise.all,allSettled,race, andanysemantics. - Nathaniel J. Smith — Notes on structured concurrency — The 2018 essay that named "structured concurrency" and motivated nurseries / task groups.