A paper from Brown University's PL group, "A Design Space Exploration of Async/Await" by Gavin Gray, takes a systematic look at a feature most programmers treat as settled: async/await. The takeaway is unsettling for anyone who assumes the semantics are basically the same everywhere. A program that fires off a log write in the background and prints "C" after a one-second sleep produces at least four different outputs depending on the runtime — and across three variations of that same program, no two of the seven runtimes studied agree on every output.

Python PEP 492 documentation page showing async/await specification design space exploration
Python Enhancement Proposal 492 formally defines async/await semantics, separating native coroutines from generators to enable static analysis in Python runtime implementations.peps.python.org

The article discusses how Python's PEP 492 deliberately separated native coroutines from generators, making async code statically distinguishable and linter-friendly while acknowledging the trade-offs between ergonomics and semantic control across different runtime implementations.

The paper frames the disagreement around nine "design dimensions," grouped into three phases of a task's life. Start-of-life dimensions govern how an async call is evaluated — lazily (Python, Rust), eagerly in the current thread (C#, JavaScript), or with suspension guarantees that vary by language. End-of-life dimensions cover task extent (indefinite vs. scoped to the spawning function), reference strength, and how a task is cleaned up (awaited, cancelled, or terminated outright). Cancellation dimensions — awareness, direction, and persistence — determine whether and how a task can respond to being cancelled.

Two dimensions explain the headline divergence. Dynamic-extent languages (Swift, Python+Trio) don't let a spawned task outlive the function that created it. But at the end of that scope Swift cancels the task while Trio waits for it — so Swift prints "AC" and Trio prints "ABC" for the same source text. The paper formalizes these choices in a small-step operational semantics, tracing exactly where each runtime forks.

The supporting language specs confirm these are deliberate, not accidental. Python's PEP 492 separated native coroutines from generators to make async code statically distinguishable and linter-friendly. Rust's async/await RFC (2394) frames the feature as ergonomic sugar over futures, where an async function returns an anonymous Future type that only runs when polled. Both design documents acknowledge the trade-offs the taxonomy names — ergonomics vs. control, performance vs. semantics — and neither claims a single universally correct answer.

The practical consequence: code that works correctly in one runtime can silently change behavior when ported, and a "fire-and-forget" pattern that looks safe in one language may leak, cancel, or block in another. The authors argue the dimensions are configurable per-axis in principle, but that in practice each language commits to a point in the space and knowledge doesn't transfer cleanly — a concern the Rust RFC itself acknowledges when it notes that fully programmable axes turn each library into its own async DSL.