Synchronous and asynchronous execution
Whether a call makes its caller wait for the result (synchronous) or hands control back at once and delivers the result later (asynchronous), and the ways languages let code wait without holding a thread.
How it works
A synchronous call finishes before the caller’s next line runs. If it reads from a socket and no data has arrived, the thread sleeps in the kernel until it does: the call blocks. A non-blocking call returns at once, with the data or with “not ready yet”, and something has to tell the program later that the data is there. Asynchronous code is built on that: the caller starts an operation, gets control back, and the result arrives later through a callback, a future, a resumed await, a channel or a message. What it buys is waiting without holding a thread. By Little’s law, a service with 50 ms of latency serving 200 requests per second has 10 requests in flight; if each holds an operating-system thread for its whole life, the thread count, not the CPU, becomes the limit, because OS threads are costly. What it costs: in the asynchronous style each request hops between threads, so stack traces lose their context and debuggers can’t step through a request, and with async/await functions split into two “colours”, because an async function returns a promise or task that only another async function can await. There are two families of answers. One makes waiting explicit in the code: callbacks, futures and promises, async/await. The other makes threads so cheap that blocking is fine again: goroutines, Java virtual threads and Erlang processes, which pass results over channels or into mailboxes. None of this is parallelism. Concurrency is dealing with many things at once; parallelism is doing many things at once. A Node.js process handles many connections concurrently on one thread, but it runs one callback at a time, and CPU-bound work gets no faster from being asynchronous.
Ordering coffee. Synchronous: you stand at the counter until your drink is made, and the queue behind you waits too. Asynchronous: you’re handed a buzzer (a future), you sit down, the barista takes the next order, and the buzzer goes off when your drink is ready. Parallelism is hiring a second barista.
Reach for it when
- Work that mostly waits on the network, disks or other services: API gateways, proxies, crawlers, chat servers
- Starting several independent calls and waiting for all of them (
Promise.all,Task.WhenAll,asyncio.gather, a goroutine each) - Keeping a UI thread responsive while data downloads
- Thousands of concurrent connections, where a platform thread per connection would run out of memory or threads
Think twice when
- CPU-bound work such as image processing or number crunching; asynchrony adds no cores, so use threads or processes for parallelism
- Small scripts and low concurrency, where plain blocking code is easier to read and debug
- Calling blocking libraries from async code on an event loop: one blocking call stalls every task on that thread
- Wrapping everything in futures in Go, Erlang or Java 21+, where cheap threads already let blocking code scale
A real example
A product page needs the user, their orders and recommendations from three services, each taking about 100 ms. Called one after another, the page takes 300 ms and holds a thread the whole time. Started together and awaited with Promise.all (or three goroutines), it takes about 100 ms: as long as the slowest call. With a pool of 200 platform threads that each block for 300 ms, the server tops out near 667 requests per second (200 ÷ 0.3 s) however idle its CPUs are. Written asynchronously, or run on virtual threads, a waiting request costs a small object on the heap instead of a thread.
More on synchronous and asynchronous execution
Narrower ideas that build on this one.
Blocking, non-blocking, asynchronous, parallel#
These four words describe different things. Blocking and non-blocking describe one call: does the thread stop until the operation completes, or does the call return at once? Synchronous and asynchronous describe how the result reaches the caller: as the return value, or later, through a callback, a future, an await, a channel or a message. Parallel describes the hardware: two pieces of work running at the same instant on different cores.
Microsoft’s C# guide uses breakfast to separate them. One cook can make eggs, bacon and toast asynchronously: start the eggs, and while they cook, put the bread in the toaster. Nothing runs in parallel, because there is one cook, yet breakfast is ready far sooner than if the cook stared at each pan until it finished. Parallel cooking needs a second cook. Rob Pike puts the same split in one line: concurrency is about dealing with lots of things at once, parallelism is about doing lots of things at once.
That is why asynchrony helps I/O-bound services and not CPU-bound ones. JEP 444 makes the point about Java’s virtual threads: they are not faster threads and run code no faster than platform threads; they exist for scale, so many more tasks can wait at once. If each task sorts a huge array instead of waiting, more threads than cores doesn’t help.
The six models side by side#
Each model is a different answer to “what happens to the caller while it waits?”
| Model | How the caller waits | Holds an OS thread while waiting? | Coloured functions? | Where you meet it |
|---|---|---|---|---|
| Callbacks | It doesn’t: it passes a function that the runtime calls later with the result | No | Yes: anything that needs the result must itself take a callback | Node.js error-first callbacks, browser event handlers |
| Futures and promises | Gets an object that will hold the value; chains steps onto it or blocks on get() | No when chained; yes if it blocks on get() | Yes: the result type is Promise<T>, Task<T>, Future<T> | JavaScript Promise, Java CompletableFuture, C# Task |
| async/await | Writes await; the function is suspended and its thread is freed | No | Yes: only an async function can await | C#, JavaScript, Python, Rust |
| Green threads | Makes an ordinary blocking call; the runtime parks the lightweight thread | No: the runtime frees the OS thread underneath | No: every function is ordinary | Go goroutines, Java 21 virtual threads, Erlang processes |
| CSP channels | Blocks on a receive from a channel until a sender delivers | Only if the channel is used from a real OS thread | No in Go; in Clojure, channel operations park only inside go blocks | Go channels, Clojure core.async, occam |
| Actor messages | Doesn’t wait: sends a message and later handles a reply message from its mailbox | No | No, but results come back as messages, not return values | Erlang processes, Akka actors |
- “Coloured functions” is Bob Nystrom’s name for the split between functions that return a value and functions that return a wrapper for one; he names Go, Lua and Ruby as languages without it, because they can switch between independent call stacks.
The same two calls in Go#
The snippet above does it with promises. Go needs no async keyword: fetchUser and fetchOrders are ordinary blocking functions. Each runs in its own goroutine and sends its result down a channel; receiving from the channel blocks only the receiving goroutine, so both waits overlap.
package main
import (
"fmt"
"time"
)
type User struct{ Name string }
type Order struct{ Total int }
// Plain blocking functions: no async keyword, no promise type.
func fetchUser(id int) User {
time.Sleep(100 * time.Millisecond) // stands in for a 100 ms network call
return User{Name: "Ada"}
}
func fetchOrders(id int) []Order {
time.Sleep(100 * time.Millisecond)
return []Order{{Total: 42}}
}
func main() {
start := time.Now()
users := make(chan User)
orders := make(chan []Order)
go func() { users <- fetchUser(7) }() // each call runs in its own goroutine
go func() { orders <- fetchOrders(7) }()
u, o := <-users, <-orders // receiving blocks this goroutine, not an OS thread
fmt.Println(u.Name, len(o), time.Since(start).Round(10*time.Millisecond))
// Ada 1 100ms
}What asynchrony costs#
Colour. Bob Nystrom’s 2015 essay describes the split: synchronous functions return values, async ones return a Task<T> or Future<T> wrapper, and you can only unwrap it by making your own function async too. async/await makes red functions much easier to call but leaves the two colours, and the colour spreads up through every caller and into higher-order functions such as map.
Lost context. JEP 444 lists what Java developers gave up by moving to the asynchronous style: each stage of a request may run on a different thread, so stack traces provide no usable context, debuggers can’t step through request-handling logic, and profilers can’t tie an operation’s cost to its caller. Virtual threads keep a real stack per request, so debuggers and thread dumps work as they do for platform threads.
Blocking the loop. On a single-threaded event loop, one callback that computes for a long time, or calls a blocking API, stalls every other task. See the event-loop concept for how Node.js and Python’s asyncio handle this.
In code · js
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function fetchUser(id) {
await sleep(100); // stands in for a 100 ms network call
return { id, name: "Ada" };
}
async function fetchOrders(id) {
await sleep(100);
return [{ id: 1, total: 42 }];
}
async function main() {
let t = Date.now();
const user = await fetchUser(7); // wait 100 ms...
const orders = await fetchOrders(7); // ...then another 100 ms
console.log("one after the other:", Date.now() - t, "ms"); // about 200 ms
t = Date.now();
const [u, o] = await Promise.all([fetchUser(7), fetchOrders(7)]); // both start now
console.log("together:", Date.now() - t, "ms"); // about 100 ms
console.log(u.name, o.length); // Ada 1
}
main();Often confused with
Where it came from1973–2023
The building blocks are older than most languages that use them. In 1973 Carl Hewitt and colleagues described a model in which every computation is actors sending messages. In 1976 Daniel Friedman and David Wise proposed the “promise”, and in 1977 Henry Baker and Carl Hewitt described call-by-future, where each argument is evaluated by its own process. In 1978 Tony Hoare’s “Communicating Sequential Processes” made sending and receiving primitive operations. Barbara Liskov and Liuba Shrira’s 1988 promises applied the idea to asynchronous calls between machines.
Mainstream servers meanwhile ran a thread per request, and OS threads proved too costly to have one per waiting connection. The way out was to stop holding a thread while waiting: event loops and callbacks, then futures (Java 5’s Future; JavaScript promises in ES2015) and finally async/await, which grew out of F#’s asynchronous workflows into C# 5.0 in 2012, Python 3.5 in 2015, ECMAScript 2017 and Rust 1.39 in 2019. In 2015 Bob Nystrom’s essay “What Color is Your Function?” named the price: every async function divides a codebase in two.
Another line of languages made threads cheap instead. Erlang, begun at Ericsson in 1986, runs every task as a tiny process with a mailbox. Go, open-sourced by Google in November 2009, put Hoare’s ideas into a procedural language with goroutines and channels, and Clojure’s core.async brought channels to the JVM and JavaScript in 2013. Java 21, released in September 2023, added virtual threads explicitly to give thread-per-request code the scalability of the asynchronous style without rewriting it.
Why it’s built this way
- Make the wait visible in the type:
Promise<T>,Task<T>,Future<T> - On an event loop the call stack is discarded while the OS does the I/O, so the program has to keep its place somewhere else. A future object, and later a compiler-generated state machine behind
await, keeps that state on the heap instead of in closures written by hand. - Make threads cheap instead of changing the code
- The asynchronous style scaled but lost stack traces, debuggers and profilers, and split every API in two. Goroutines and virtual threads keep blocking code and real stacks, and let the runtime free the OS thread underneath.
- Communicate over channels rather than shared memory
- Go’s designers found that low-level primitives such as mutexes and condition variables made concurrency hard, and that Hoare’s CSP fitted a procedural language well. Passing a value over a channel also passes its ownership, so data races can’t occur by design.
- One mailbox per actor, one message at a time
- If only messages can change an actor’s state and they’re processed one at a time, the actor’s invariants hold without locks, and senders never block.
Timeline
- 1973
Hewitt, Bishop and Steiger describe the actor model
- 1977
Baker and Hewitt describe call-by-future
- 1978
Hoare publishes “Communicating Sequential Processes”
- 1988
Liskov and Shrira’s promises for asynchronous remote calls
- 2009
Google open-sources Go, with goroutines and channels
- 2012
C# 5.0 ships
asyncandawait - 2017
ECMAScript 2017 adds async functions to JavaScript
- 2023
Java 21 ships virtual threads
- Hewitt, Bishop and Steiger: A Universal Modular ACTOR Formalism for Artificial Intelligence (IJCAI 1973)
- Wikipedia: Futures and promises (Friedman and Wise 1976; Java 5; F# asynchronous workflows)
- Baker and Hewitt: The Incremental Garbage Collection of Processes (MIT AI Lab Working Paper 149, June 1977), call-by-future
- C. A. R. Hoare: Communicating Sequential Processes (Communications of the ACM, August 1978)
- Liskov and Shrira: Promises, linguistic support for efficient asynchronous procedure calls in distributed systems (PLDI 1988)
- OpenJDK: JEP 444, Virtual Threads (Little’s law, OS threads are costly, the asynchronous style loses stack traces and debuggers)
- Java SE 21 API: Future (get waits for the result; since 1.5)
- Node.js Learn: JavaScript asynchronous programming and callbacks (error-first callbacks, nesting, Promises in ES6, async/await in ES2017)
- Microsoft Learn: The history of C#, C# version 5.0 (released August 2012 with Visual Studio 2012; async and await)
- PEP 492: Coroutines with async and await syntax (created April 2015, Python 3.5)
- ECMA-262 8th edition, ECMAScript 2017 (June 2017): introduces async functions
- Rust Blog: Async-await on stable Rust! (November 2019, Rust 1.39)
- Bob Nystrom: What Color is Your Function? (Feb 2015)
- Joe Armstrong: Making reliable distributed systems in the presence of software errors (PhD thesis, 2003), Erlang’s history from 1986
- Google Open Source Blog: Hey! Ho! Let’s Go! (10 November 2009)
- Go FAQ: Why build concurrency on the ideas of CSP? and Why goroutines instead of threads?
- Effective Go: Concurrency (share by communicating; unbuffered channels)
- Rich Hickey: Clojure core.async Channels (28 June 2013)
- OpenJDK: JDK 21 (General Availability 19 September 2023)
- Akka docs: How the actor model meets the needs of modern, distributed systems (mailboxes, one message at a time, no shared call stack)
Sources27
- OpenJDK: JEP 444, Virtual Threads (Little’s law, OS threads are costly, the asynchronous style loses stack traces and debuggers)
- Bob Nystrom: What Color is Your Function? (Feb 2015)
- The Go Blog: Concurrency is not parallelism (Rob Pike’s Waza talk, 16 January 2013)
- Node.js: About Node.js (many connections handled concurrently, without threads)
- Microsoft Learn: Asynchronous programming with async and await (breakfast example, Task.WhenAll, don’t block)
- MDN: async function (always returns a promise; await suspends and yields control)
- MDN: Promise.all() (fulfills when all fulfill, rejects on the first rejection)
- Node.js Learn: JavaScript asynchronous programming and callbacks (error-first callbacks, nesting, Promises in ES6, async/await in ES2017)
- Rich Hickey: Clojure core.async Channels (28 June 2013)
- Go FAQ: Why build concurrency on the ideas of CSP? and Why goroutines instead of threads?
- Effective Go: Concurrency (share by communicating; unbuffered channels)
- C. A. R. Hoare: Communicating Sequential Processes (Communications of the ACM, August 1978)
- Hewitt, Bishop and Steiger: A Universal Modular ACTOR Formalism for Artificial Intelligence (IJCAI 1973)
- Akka docs: How the actor model meets the needs of modern, distributed systems (mailboxes, one message at a time, no shared call stack)
- Erlang/OTP docs: Concurrent programming (processes share no data; each has its own message queue)
- Erlang/OTP docs: Efficiency guide, Processes (a new process uses 327 words)
- Erlang/OTP docs: Design principles (supervisors monitor and restart workers)
- Baker and Hewitt: The Incremental Garbage Collection of Processes (MIT AI Lab Working Paper 149, June 1977), call-by-future
- Liskov and Shrira: Promises, linguistic support for efficient asynchronous procedure calls in distributed systems (PLDI 1988)
- Wikipedia: Futures and promises (Friedman and Wise 1976; Java 5; F# asynchronous workflows)
- Java SE 21 API: Future (get waits for the result; since 1.5)
- Java SE 21 API: CompletableFuture (since 1.8)
- Microsoft Learn: The history of C#, C# version 5.0 (released August 2012 with Visual Studio 2012; async and await)
- ECMA-262 8th edition, ECMAScript 2017 (June 2017): introduces async functions
- PEP 492: Coroutines with async and await syntax (created April 2015, Python 3.5)
- Rust Blog: Async-await on stable Rust! (November 2019, Rust 1.39)
- Rust std docs: the async keyword (returns a Future; not run until awaited)