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:

  1. 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.
  2. 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.
  3. 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:

  • JavaScriptPromise.all([a, b]) resolves when both succeed, rejects on the first failure. Promise.allSettled returns every outcome regardless. Promise.race settles with whichever finishes first (success or failure). Promise.any is race for successes — it rejects only if every input rejects.
  • Pythonasyncio.gather(*tasks) is Promise.all-shaped. asyncio.wait is 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

  1. Forgotten await — calling an async function without awaiting it returns the coroutine/promise object, not the value. Linters (@typescript-eslint/no-floating-promises, ruff's RUF006) catch most cases; treat them as errors.
  2. Blocking the event loop — calling time.sleep, a sync requests.get, a CPU-bound NumPy routine, or JSON.parse on a 50 MB string from inside a coroutine freezes every concurrent task. Use asyncio.to_thread / loop.run_in_executor (Python), worker_threads (Node), or just declare the route def so the framework runs it in a thread pool (FastAPI does this automatically for def routes).
  3. Promise.all short-circuits on rejection — one failure rejects the aggregate while sibling promises keep running in the background, often unhandled. Use Promise.allSettled when partial results are acceptable, or wrap each task in a .catch.
  4. No cancellation discipline — without structured concurrency, a for await that throws mid-loop leaves background tasks orphaned. Use TaskGroup / Trio nurseries / AbortController so cancellation propagates to siblings.
  5. GIL still applies in Pythonasyncio does not sidestep the Global Interpreter Lock. It overlaps I/O wait, not CPU work. For CPU-bound parallelism use multiprocessing, concurrent.futures.ProcessPoolExecutor, or the no-GIL build (PEP 703).
  6. "Async coloring"async is contagious: an async callee forces every caller up the chain to also be async (or block with asyncio.run / .then). Decide early which layers are async and resist sprinkling it mid-codebase.
  7. AsyncClient lifetime 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 with async with or an explicit await client.aclose().

Where to go next

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 / await keywords on top of asyncio.
  • 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.TaskGroup and the AnyIO API.
  • PEP 654 — Exception Groups and except* — The language-level support that made TaskGroup ergonomic 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, and any semantics.
  • Nathaniel J. Smith — Notes on structured concurrency — The 2018 essay that named "structured concurrency" and motivated nurseries / task groups.