We say “async/await” as if it were one feature. A Brown CEL team (Gavin Gray, with the accompanying paper) treats it instead as a design space of nine independent decisions, and shows that seven Runtimes — Python/Asyncio, Trio, Tokio, Smol, JavaScript, C#, Swift — disagree on all of them in observable ways. They call the paradigm straight-line asynchrony: concurrency that looks like straight-line code.
The demo program is trivial. One function logs “A”, sleeps, logs “B”; another spawns it without awaiting; main sleeps a second and logs “C”:
- Seven runtimes produce four different outputs.
- Across three variations of that program, no two runtimes agree.
- The paper’s conclusion: you were probably right about async semantics for some language — just not yours.
The nine dimensions, grouped by a task’s lifetime:
- Start of life — Eagerness (lazy: Python, Rust return an inert coroutine; eager: C#, JavaScript start on spawn) and Suspension (static: await always suspends, JavaScript; dynamic: no such guarantee, everyone else).
- End of life — Extent (indefinite: tasks may outlive their spawning scope, JavaScript/C#/Tokio/Smol/Asyncio; dynamic: they may not, Swift and Trio), Reference strength, Destruction (awaited in Trio, cancelled in Swift/Tokio/Asyncio, terminated in C#), and Propagation (Trio reraises exceptions from unawaited tasks; everyone else swallows them into the task).
- Cancellation — Awareness (Rust tasks cannot respond to cancellation at all), Direction (top-down in Rust, bottom-up in Asyncio/Trio, simultaneous in Swift), Persistence (transient in Asyncio, persistent in Trio/Swift).
The worked example falls out of two of them. Swift and Trio both chose dynamic extent, so the spawned task cannot outlive the function that spawned it — but Swift cancels it (prints “AC”) while Trio politely awaits it (prints “ABC”). Same principled position, different program.
The paper formalizes the table into a small-step semantics over a core calculus, so a trace shows exactly which rules are shared and which are forks in the road.
The practical value is the table, not the verdict. Nobody is wrong here — each runtime made trade-offs across performance, memory, ergonomics, and semantics. But “does an unawaited task outlive the scope that created it?” and “can this task be cancelled?” have different answers in Swift, Tokio, and Asyncio, and those are exactly the questions that decide whether a fire-and-forget write survives your request handler.