logo
Kurashizu Blogwhere ideas flow
  • Home
  • News
  • Blog
  • About
  • Home
  • News
  • Blog
  • About

© 2026 Kurashizu. All rights reserved.

Admin·New Post·Service Status
Powered by Cloudflare·31.10.26
← Back to news

A Design Space Exploration of Async/Await

Original article
cel.cs.brown.edu|389 points|113 comments|by wcrichton|Sep 9, 2026

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

CategoryDimensionDescriptionOptions & Language Examples
Start of LifeEagernessHow a function call is evaluated.Eager: (Python, Rust) →\to→ Eval in current thread, schedule on await.<br>Suspension: (C#, JS)
SuspensionDo await points actually suspend?(C#, Swift, Tokio, Smol, Asyncio, Trio)
End of LifeExtentHow long a task can exist by default.Indefinite: (JS, C#, Tokio, Smol, Asyncio) →\to→ Until runtime ends.<br>Dynamic: (Swift, Trio) →\to→ Until spawning scope ends.
Ref. StrengthType of reference held by runtime (for Indefinite).Weak: (JS, C#, Tokio)<br>Strong: (Asyncio, Smol)
DestructionCleanup process at the end of extent.Cancelled: (JS, Trio) →\to→ Cancelled then possibly awaited.<br>Other: (C#)
PropagationHandling exceptions in unawaited tasks.(JS, C#, Tokio, Smol, Asyncio, Swift)
CancellationAwarenessCan a task respond to cancellation?(Asyncio, Trio, Swift)
DirectionHow cancellation flows through the graph.Top-Down: (Rust)<br>Bottom-Up: (Asyncio, Trio)<br>Simultaneous: (Swift)
PersistenceDoes cancellation "stick" if ignored?Transient: (Asyncio) →\to→ Can proceed normally.<br>Persistent: (Trio, Swift) →\to→ 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:

  1. Swift →\to→ Uses Cancelled Destruction. It kills the task immediately.
  2. Trio →\to→ 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: S→S′→S′′S \to S' \to S''S→S′→S′′

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.

Formal Model Trace

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.