- The paper presents a comprehensive exploration of 'straight-line asynchrony,' revealing significant design disparities among async/await mechanisms in seven prominent languages.
- The nine-dimensional design space encompasses eagerness, suspension, reference strength, destruction, propagation, and cancellation awakness, among others.
- The study concludes by demonstrating that identical-looking async programs yield varied outputs across systems, and models these systems using an executable calculus to ground claims empirically.
The paper presents a systematic design space exploration of "straight-line asynchrony"—the async/await paradigm found in JavaScript, C#, Swift, Python, and Rust. Its central finding is that despite shared keywords and superficially similar descriptions, no two of the seven surveyed language/runtime combinations agree as a whole on the design decisions that govern when and in what order asynchronous work executes. The authors demonstrate this divergence with a small motivating example—a spawned task writing to a log, invoked under three different calling contexts—whose output varies across all seven systems, and they account for these differences both informally and via an executable operational semantics (2608.20677).
Motivation and scope
The paper situates async/await as a response to the inversion of control inherent in callback- and event-loop-based asynchrony: the goal is code that "reads like a sequence of statements, but executes in a more complicated order." The authors define asynchrony as a special case of cooperative concurrency—interleaving occurs because subcomputations are written to yield control to one another—in contrast to competitive concurrency imposed externally by OS threads or interrupts. Straight-line asynchrony is characterized as eliminating complex control flow; the term deliberately avoids "task asynchrony" because tasks are not universal across designs (Rust exposes coroutines without requiring tasks; Trio hides tasks entirely).
The study covers languages with settled, widely used designs: JavaScript (ECMAScript 2025), C# 14 on .NET 10, Swift 6.2 (structured-concurrency subset), Python 3.14 with Asyncio and Trio v0.33, and Rust 1.92 with Tokio v1.50 and Smol v2.0.2. The analysis was grounded in design documents (RFCs, proposals), GitHub discussions, community blogs, and documented reports of surprising behavior. Only calls that introduce concurrency are considered; immediately-awaited applications behave synchronously under every design studied.
A case study on timeout
Before presenting dimensions, the paper develops timeout as a paradigmatic example distinguishing minimal semantics (return first result or indicate timeout) from enhanced semantics incorporating cancellation: if f exceeds the duration it is cancelled; if the timeout itself is cancelled, so is f; and cancelled work can run cleanup code. Achieving this requires substantively different mechanisms across systems: library-level cancellation tokens threaded manually through code (C#, also available in JS and Rust), versus language-level structured constructs where cancellation propagates through task groups (Swift) or nurseries (Trio). Notably, Python+Trio provides no racing primitive at all, unlike the other systems sampled.
Nine design dimensions
The core contribution is a taxonomy of nine dimensions grouped into start of life, end of life, and cancellation:
| Category |
Dimension |
Points |
| Start of life |
Eagerness |
Lazy / Eager / Semi-eager |
| Start of life |
Suspension |
Static / Dynamic |
| End of life |
Extent |
Indefinite / Dynamic (scoped) |
| End of life |
Reference strength |
Strong / Weak runtime handle |
| End of life |
Destruction |
Awaited / Cancelled / Terminated |
| End of life |
Propagation |
Destructive reraise / Never |
| Cancellation |
Awareness |
Aware / Unaware |
| Cancellation |
Direction |
Top-down / Bottom-up / Simultaneous |
| Cancellation |
Persistence |
Transient / Persistent |
Eagerness concerns what happens when an async function is called. Lazy designs (Python, Rust) return a coroutine without executing anything; eager designs (C#, JavaScript) run the body synchronously until the first await, then return a task; semi-eager designs schedule the task immediately for another thread (Swift's async let). The paper classifies Swift as both eager and semi-eager because it offers syntax for both and has no default—a bare async application is a type error. Lazy asynchrony buys predictable allocation (stack-allocatable coroutines) and user-swappable runtimes (e.g., Embassy vs. Tokio), at the cost of ecosystem fragmentation, since spawn is runtime-specific, and of programmer surprise: calling a function and having nothing happen contradicts synchronous intuition.
Suspension distinguishes JavaScript—the only surveyed system where await points are statically guaranteed to yield per the ECMAScript specification—from all others, which suspend dynamically depending on whether the awaited value is ready. Static suspension prevents starvation (an eagerly-started task completing entirely synchronously would otherwise become CPU-bound) but costs a round-trip to the runtime when awaiting already-completed tasks.
Extent separates indefinite-extent designs (JS, C#, Asyncio, Tokio, Smol), where tasks may live until end of runtime, from dynamic-extent designs (Swift, Trio), where tasks are tied to a spawning scope and destroyed when the parent completes. Dynamic extent composes better with resource management (e.g., file descriptors within a with block) but existing implementations enforce strictly hierarchical parent-child relationships, disallowing patterns like two parents awaiting one child.
Reference strength asks whether the runtime's handle keeps a task alive. JS, C#, and Tokio hold strong references; Smol achieves weak-reference behavior via Drop destructors on task handles (effectively dynamic extent unless the handle escapes); Asyncio holds weak references—a behavior flagged as a bug on CPython's tracker, with Guido van Rossum conceding the original rationale is not recalled even by its designers.
Destruction distinguishes awaiting a task to completion (JS, Trio), cancelling then possibly awaiting it (Swift, Tokio, Smol, Asyncio), or simply terminating the program while tasks run (C#). The dimension is orthogonal to extent. A structural limitation arises for Smol: Rust's Drop permits only synchronous code, so drop-based destruction cannot await cancelled tasks.
Propagation concerns exceptions raised in unawaited tasks. Every system except Trio adopts never-propagation (at most a stack trace printed; Node.js exits non-zero, though this is unspecified in ECMAScript). Trio alone reraises dependency failures when the nursery scope ends, making silent failures harder to ignore—at the cost of violating the common architectural expectation that errors stay encapsulated at task boundaries (e.g., one bad web-server connection should not crash the server).
Awareness contrasts unaware cancellation (Rust: a cancelled coroutine is simply never polled again) with aware cancellation (Python: a cancellation exception thrown into the task, catchable for cleanup; Swift: a cancellation flag observable via cancelled?). Unaware cancellation guarantees eventual cancellation of compute-bound tasks but can break logical invariants mid-critical-region; the paper notes Rust's workaround via Drop handlers is verbose, ownership-complicating, and synchronous-only.
Direction describes how cancellation traverses the task graph: top-down via deterministic drop order (Rust/Smol), bottom-up by throwing exceptions into leaf dependencies that propagate upward (Asyncio/Trio), or simultaneous broadcast of a shared flag (Swift). Bottom-up gives individual functions leeway to swallow cancellation; simultaneous ensures every task knows it is cancelled even if an exception was caught, but requires representing cancellation both formally (flag) and conventionally (exception).
Persistence, applicable only to aware designs, separates transient cancellation (Asyncio: once a cancellation exception is delivered and handled, subsequent awaits proceed normally and newly spawned tasks are unaware of the prior request) from persistent cancellation (Trio/Swift: any later await may again raise cancellation). Persistent designs pair with shielding operators (e.g., Trio's shield) enabling bounded asynchronous cleanup such as TLS close_notify.
To give a precise holistic account, the paper models each language as a layered extension of a core calculus λC​ with mutable references and delimited continuations (shift/reset), plus an exception extension λexn​ featuring a throw-in form modeled after Python's coroutine .throw() method. An async platform λasync​ adds labeled async frames (frame stacks as threads), a process collection, a task queue Q, and a signals queue T of timed continuations modeling nondeterministic-duration I/O via sys-io. Languages without native coroutines (Swift, C#, JS) build their runtimes into the platform; lazy languages (Python, Rust) extend it only via their runtimes' spawn, cancel, and related operations.
Rather than faithfully reproducing state-machine implementations, all seven systems are unified through delimited continuations, and reduction rules are presented side-by-side with diverging fragments color-coded by design dimension—for example, the Async-App rules differ precisely in eagerness, extent epilogues (Swift cancels and waits on dependencies; C#/JS do neither), and exception propagation (Trio's Spawn adds wait-on-dependencies uncancelled plus failure reraising). The Await rule makes the static/dynamic suspension distinction a single syntactic difference in where the continuation capture sits relative to the readiness check.
Crucially, the model is executable in PLT Redex with a test suite covering all paper examples and a differential fuzzer generating random programs, compiling them to real languages, and checking outputs match the model over 50 runs each (accounting for nondeterminism via subset matching). This grounds the formal claims empirically. The paper also shows a branching evaluation tree explaining why each of the seven runtimes produces its particular output for the motivating example—including cases where different languages reach the same output for different reasons (C#'s terminated tasks vs. Swift's cancelled ones).
Limitations and open questions
The paper is candid about several boundaries. The nine dimensions focus on functionality-affecting decisions and explicitly exclude ergonomics (await as prefix/postfix) and pure performance questions, though the authors acknowledge performance and functionality are difficult to disentangle here. The reference-strength dimension is elided from the presented reduction rules for space, handled only in supplemental material, and the presentation leans on metafunctions and macros. Coverage of surrounding languages (Kotlin, C++20, F#, Haskell, OCaml, Hack, Nim, Dart, Zig) is deferred to discussion rather than full classification—C++20 cannot be placed on the taxonomy at all because every axis is library-configurable, making cross-project knowledge transfer difficult. Kotlin's automatic suspension points and Zig's colorblind async are discussed qualitatively; the paper notes Kotlin's claimed benefits are "not accompanied by an evaluation."
Most significantly, the authors state that empirical data supporting the trade-offs is missing: how much overhead semi-eager application incurs versus eager, whether simultaneous cancellation prevents bugs bottom-up cancellation allows, which semantics best matches developer expectations, and which decisions lead to more errors—all remain open questions they hope the taxonomy will motivate.
Conclusion
This paper converts a landscape of confusingly similar async/await designs into a structured nine-dimensional space spanning task lifecycles and cancellation, backed by executable Redex semantics validated against seven real runtimes via differential fuzzing. Its demonstration that identical-looking programs produce divergent outputs across all surveyed systems—and that some identical outputs arise from divergent mechanisms—provides a concrete vocabulary for developers porting knowledge between languages and a checklist for designers. The artifact, including runnable examples in each language and the fuzzer, positions the taxonomy as shared ground for future comparative and empirical study of asynchronous semantics.