Asynchronous programming

On this page 7

Asynchronous programming is a programming model in which a program starts an operation that takes time, such as a network request, a disk read or a timer, and continues with other work instead of blocking until it completes; the result is handled later through a callback, a future or an awaited coroutine. In concurrency, it is the main technique for handling many simultaneous waits with few threads.

Asynchronous code is the foundation of Node.js, Python’s asyncio, C# and JavaScript async/await, Kotlin coroutines and Rust’s async ecosystem. It is designed for I/O-bound workloads, where most of the time is spent waiting, and it should not be confused with parallelism, which runs computations at the same instant on several cores.

How asynchronous programming works

At the center of most asynchronous runtimes is an event loop: a single thread that repeatedly takes ready tasks from a queue, runs each until it must wait, and then moves to the next. Waiting is delegated to the operating system through non-blocking I/O interfaces such as epoll on Linux, kqueue on BSD and macOS, IOCP on Windows and io_uring on newer Linux kernels.

When an operation completes, the operating system notifies the runtime, which schedules the continuation of the waiting task. The task model is cooperative: a task gives up control only at explicit suspension points, usually an await expression. This is why one event loop thread can keep tens of thousands of connections open while using little memory per connection.

Languages implement async functions by compiling them into state machines. Each await becomes a state in which the function can pause, store its local variables and later resume exactly where it stopped. To the programmer, the code reads sequentially, while the runtime interleaves many such functions.

From callbacks to async and await

Asynchronous programming has evolved through several generations of abstractions, each fixing readability or error handling problems of the previous one.

ModelHow results are deliveredStrengthWeakness
CallbacksFunction called on completionSimple, no runtime support neededNested callback pyramids, scattered errors
Promises and futuresObject representing a pending valueChaining and compositionLong chains still hard to read
Async and awaitSuspension inside sequential-looking codeReadable, normal error handlingAsync spreads through call chains
Reactive streamsObservable sequences of eventsOperators for streams and backpressureSteep learning curve
Virtual threads and green threadsRuntime-managed lightweight threadsBlocking style without blockingRequires runtime support, pinning pitfalls
Main asynchronous programming models

Modern code mostly uses async and await on top of promises or futures. Java took a different path with virtual threads, standardized in Java 21, and Go uses goroutines: both let developers write ordinary blocking code while the runtime multiplexes it onto a few operating system threads.

Asynchrony compared with threads and parallelism

Asynchronous programming, threads and parallelism are related but distinct concurrency tools, and choosing between them depends on whether the workload waits or computes.

ApproachUnit of workBest forMain risk
Asynchronous programmingTask or coroutine on an event loopMany concurrent I/O waitsBlocking calls stall everything
Operating system threadsThread with its own stackModerate concurrency, blocking librariesMemory cost, race conditions
ParallelismWork split across coresCPU-bound computationCoordination overhead
MultiprocessingSeparate processesIsolated CPU work, crash containmentExpensive communication
Asynchronous programming versus neighboring concurrency approaches

A single-threaded event loop avoids many data races, because tasks never run at the same moment, but it does not eliminate logical race conditions: state can still change between two await points. Deadlocks also remain possible, for example when synchronous code blocks while waiting for an asynchronous result that needs the same thread.

Practical patterns

Effective asynchronous code relies on a small set of composition patterns that control how many operations run and how long they may take.

  • Concurrent fan-out: start several independent requests and await them together, as with Promise.all or asyncio.gather.
  • Timeouts and cancellation: bound every external call, using cancellation tokens in C# or task cancellation in Python.
  • Bounded concurrency: limit simultaneous operations with a semaphore to protect downstream services.
  • Structured concurrency: tie child tasks to a parent scope, as task groups in Python and Kotlin coroutine scopes do, so none outlive their owner.
  • Offloading CPU work to a thread or process pool so the event loop stays responsive.

Structured concurrency is increasingly considered the default, because it makes errors and cancellations propagate predictably and prevents orphaned background tasks that silently fail.

Pitfalls and how to diagnose them

Asynchronous bugs are often intermittent and load-dependent, which makes them hard to reproduce. The table lists the classic problems.

SymptomCauseFix
Whole service freezes under loadBlocking call on the event loopUse async libraries or offload to a pool
Errors vanish silentlyUnawaited promise or taskAlways await or attach error handlers
Application hangs on startupSync-over-async deadlockStay asynchronous end to end
Downstream service overwhelmedUnbounded fan-outSemaphores, queues and rate limits
Stack traces lack contextContinuations lose call historyAsync-aware debuggers and tracing
Common asynchronous programming problems

The so-called function coloring problem is a structural cost: an asynchronous function can usually be called only from other asynchronous code, so adopting async in one layer tends to spread through the codebase. Teams should decide early which layers are asynchronous rather than mixing models ad hoc.

Explained without jargon

Imagine a cook preparing a dinner. A cook who waits beside the oven wastes time. A good cook puts the roast in, sets a timer, and chops vegetables while it bakes. When the timer rings, the cook returns to the roast.

Asynchronous programming works the same way. The program starts a slow task, such as fetching data from the internet, and does other useful work while it waits. One worker can then juggle many jobs, because most of each job is waiting.

A typical use case

A travel comparison website must query eight airline partner APIs for each search, and each partner responds in 300 to 1,500 milliseconds. The original synchronous implementation called them one after another, so a search took up to ten seconds and each request tied up a server thread.

The team rewrites the search handler with async and await, fans out all eight requests at once, gives each a two-second timeout and limits concurrency per partner with a semaphore. Slow partners are shown as unavailable instead of delaying the page. Search time drops to about the slowest single response, roughly 1.5 seconds, and one server instance now handles several times more concurrent searches with the same memory.