A Design Space Exploration of Async/Await
Analyzing the Design Space of Async/Await
By Gavin Gray | September 8, 2026 | Cognitive Engineering Lab
Many modern programming languages have adopted the async and await keywords to handle concurrency. The primary goal is consistent: transform concurrent logic into something that resembles callback hell straight-line code.
We define this paradigm—where asynchronous operations are written as if they were sequential—as straight-line asynchrony.
This approach has been evolving for over 15 years. Our research project aimed to answer a fundamental question: How much does the implementation of async/await actually vary between different languages?
To explore this, we published a paper titled "A Design Space Exploration of Async/Await."
The Paradox of Predictability
To illustrate the divergence in modern languages, consider this pseudocode example. One function handles a slow log write, while another triggers that write as a background task and immediately returns.
async fn write_to_log() {
print("A")
await sleep(2) // Simulate latency
print("B")
}
async fn fire_and_forget() {
task = spawn write_to_log()
// Returns immediately without awaiting 'task'
}
async fn main() {
await fire_and_forget()
await sleep(1)
print("C")
}
What is the output? If you guessed a specific sequence, you were likely correct—but only for one specific language. When we ran this across seven different async runtimes, we found four different outputs. In fact, across three variations of this program, no two runtimes produced the same result.
Defining the Design Dimensions
You might have heard influencers discuss "hot" vs. "cold" async calls.
- Hot starts: The task begins executing immediately upon creation.
- Cold starts: The task is an inert object that only runs once it is
awaited.
We categorize these types of decisions as design dimensions: choices that alter the observable behavior (semantics) of a program rather than just its speed. We call this specific dimension "Eagerness."
Our research identified nine such dimensions, which we have organized into three lifecycle stages:
📋 Project Objectives
- Identify key design dimensions of
async/await. - Map modern languages to these dimensions.
- Create a formal semantic model to predict behavior.
The Async Design Space Matrix
| Category | Dimension | Description | Options & Language Examples |
|---|---|---|---|
| Start of Life | Eagerness | How a function call is evaluated. | Eager: (Python, Rust) Eval in current thread, schedule on await.<br>Suspension: (C#, JS) |
| Suspension | Do await points actually suspend? | (C#, Swift, Tokio, Smol, Asyncio, Trio) | |
| End of Life | Extent | How long a task can exist by default. | Indefinite: (JS, C#, Tokio, Smol, Asyncio) Until runtime ends.<br>Dynamic: (Swift, Trio) Until spawning scope ends. |
| Ref. Strength | Type of reference held by runtime (for Indefinite). | Weak: (JS, C#, Tokio)<br>Strong: (Asyncio, Smol) | |
| Destruction | Cleanup process at the end of extent. | Cancelled: (JS, Trio) Cancelled then possibly awaited.<br>Other: (C#) | |
| Propagation | Handling exceptions in unawaited tasks. | (JS, C#, Tokio, Smol, Asyncio, Swift) | |
| Cancellation | Awareness | Can a task respond to cancellation? | (Asyncio, Trio, Swift) |
| Direction | How cancellation flows through the graph. | Top-Down: (Rust)<br>Bottom-Up: (Asyncio, Trio)<br>Simultaneous: (Swift) | |
| Persistence | Does cancellation "stick" if ignored? | Transient: (Asyncio) Can proceed normally.<br>Persistent: (Trio, Swift) Remains cancelled. |
Case Study: Swift vs. Trio
Two dimensions are critical for our fire_and_forget example: Extent and Destruction.
Most languages use Indefinite Extent, but Swift and Python+Trio use Dynamic Extent. This means the write_to_log task cannot outlive the fire_and_forget function that spawned it.
However, they handle the end of that scope differently:
- Swift Uses Cancelled Destruction. It kills the task immediately.
- Trio Uses Awaited Destruction. It waits for the task to finish.
The Result:
- Swift prints:
AC - Trio prints:
ABC
Formalizing the Chaos
Because these trade-offs (memory, ergonomics, performance) make predicting output difficult, we developed a formal semantics based on a core calculus.
We represent the execution as a series of small-step reductions:
By tracing these reductions, we can see exactly where a program's path diverges. In our model, most rules are universal, but the "design dimensions" act as forks in the road.
As shown in the trace, the execution of async fn write_to_log follows a standard path until it hits a highlighted rule—a specific design decision—which then steers the program toward one of the four possible outputs.