In September 2026, a Brown University lab (Gavin Gray, Shriram Krishnamurthi, Will Crichton) published "A Design Space Exploration of Async/Await" — arXiv 2608.20677. The core experiment fits in one paragraph: take a tiny async program — parent spawns child task, both print a letter — and run it on seven modern runtimes with identical async/await syntax. Result: four different outputs. Across three program variations, not a single pair of languages matched. C# and Tokio — both "async/await," print different things. JS and Tokio — same.
This isn't a provocation post, but peer-reviewed work (PACMPL track, accepted June 10, 2026) with a formal model in PLT Redex and a differential fuzzer: generates random programs, compiles them to real languages, and checks output against the model — mechanically finding semantic divergences. The authors posted it themselves on HN, and the discussion temperature was predictable: experienced Ruby developer bradleybuda took the mental quiz "what will this code print," got the verdict "you must be a JavaScript developer" — and indignantly confirmed: yes, and that's exactly why I'm ashamed I didn't know this.
My favorite comment from the discussion — from someone teaching their ten-year-old async calls to work around a platform bug in makearcade. The kid said he doesn't understand. Father's response: "Don't worry, adults don't understand either." Let's talk about those "adults."
Everyone writing Python, Rust, Swift, or JS carries the conviction that async/await is "just syntax": a convenient wrapper over promises and callbacks. Brown's paper breaks this conviction with a picture of nine axes, and languages diverged along each one.
The first and most treacherous axis — eagerness of start. When you write an async function call — has the body already started executing or not yet? In C#, JavaScript, and Swift — yes: the call executes in the current thread and schedules the task on await ("eager"). In Python-asyncio and the Rust stack (Tokio, Smol) — no: an async function is just a lazy task description ("lazy"), an inert object that silently does nothing until explicitly started. This is a shock for newcomers used to eager worlds.
Second axis — suspension. JS is the only one of seven where await guaranteed suspends execution and yields to the event loop, even if the awaited object is already resolved. This is baked into the ECMAScript spec (§27.7.5.3). Why? Starvation protection: a greedy task can't monopolize the thread even at "instant" await points. The price — an "expensive round trip" through the event loop on every await. In C#, Swift, Tokio, asyncio, and Trio, await is dynamic: if the result is ready, execution continues without going to the runtime. The same program prints ABCAB in JS, AABBC in C#.
Third — task lifetime (extent). JS, C#, Tokio, Smol, asyncio — indefinite: a task can live until runtime end. Swift and Trio — dynamic: a task doesn't outlive its spawning scope. Most interesting — comparing Swift and Trio. Both have dynamic extent, but different "destruction": Swift — cancelled (at scope end the task is cancelled — prints "AC"), Trio — awaited (parent politely waits for "write_to_log" — prints "ABC"). The difference is one word — but two incompatible programming models on output.
Fourth — fate of exceptions (propagation): in non-awaited tasks, exceptions are re-thrown by dependents only in Trio; for everyone else (JS, C#, Tokio, Smol, asyncio, Swift) they stay inside the task forever — "drop it on the floor and cross your fingers," as Nathaniel Smith writes.
Fifth — cancellation: Rust — unaware (task cannot react to cancellation) with top-down direction via Drop; asyncio and Trio — aware (cancellation exception) with bottom-up direction; Swift — aware with simultaneous (flag to all transitive dependencies at once). Plus persistence: asyncio — transient (can ignore cancellation and live as if nothing happened), Trio and Swift — persistent (can ignore, but task remains "cancelled," next await throws again).
Plus reference strength — and here begins the most shameful chapter in asyncio history.
In asyncio, loop.create_task() returns a Task, but if you didn't save the reference — Python can garbage-collect the task and silently kill it at a random moment. The diagnostic sounds like a tombstone: "Task was destroyed but it is pending!". Discord.py hit this in prod; Electrum and Home Assistant maintain their own "crutch" registries of strong references.
September 2022: alexhartl opens issue #91887 in CPython (following up on Vincent Bernat's #88831). February 2023: Guido van Rossum comes to the thread and writes — I quote verbatim — "I'm not sure why we designed it this way, and (from private email) @1st1 doesn't seem to recall either." Translation: even the creator of asyncio doesn't remember why tasks live on weak references, and Yury Selivanov (author of the asyncio modernization) — neither. A year later, February 2024, Guido returns: "I have a feeling we need a new champion to drive a PR here." Asyncio developers still can't agree what to do: a global (or per-loop) set of strong references to active tasks — the solution is simple, but "a bunch of details that need sorting through."
This is the perfect illustration of Brown's thesis: nine axes — not nine wise decisions. Some of them are accidents that became standard. Asyncio's weak references — not a conscious design choice, but a detail that accidentally got into 2012 code and has lived for over ten years. In Brown's paper, on the axis map, asyncio is marked "weak" — as a matter of bug, not intent.
The first async/await didn't appear in the mainstream. October 10, 2007, Don Syme publishes "Introducing F# Asynchronous Workflows": async { return 10+10 } produces a value of type Async<int>, "These values are not actual results: they are specifications of tasks to run" — the same lazy philosophy as Rust 12 years later, only through monadic computation expressions. April 12, 2010, this became F# 2.0 with let!. C# and VB took the keywords async/await on September 12, 2012 (before that — the painful Async CTP for VS2010, which broke with every studio update: "Until they fixed it, and then it worked. Until the next VS2010 update, when it broke again. No, seriously, it was that bad!"). Then the epidemic: Python PEP 492 (2015), JS ES2017, Rust RFC 2394 (stable 2019), Swift 5.5 (2021), Kotlin suspend (1.3, 2018), C++20.
In parallel grew an alternative trunk — structured concurrency. In April 2018, Nathaniel Smith publishes "Notes on structured concurrency, or: Go statement considered harmful" — a direct heir to Dijkstra 1968: spawning tasks "into the void" (goroutine, threading.Thread, Task.Run, Promise) is the goto of our times: control flow splits, one side makes a one-way jump, the other returns to caller. "Go statements break error handling" — exceptions rely on the concept of "caller," and after spawning a background task, "caller" disappears; "every mainstream concurrency framework I know of simply gives up" — a background task's error drops on the floor, "if you're lucky it might print something on the console." "The only other software where 'print and keep going' is a normal error handling strategy is grotesque old Fortran libraries." And finishes: even Rust — "the language voted Most Obsessed With Threading Correctness by its high school class" — "discards the error and hopes for the best" if a background thread panics.
Smith's answer — nursery: a parent can't spawn children until it creates a place for their life; a function won't return until all children finish — "when a function does return, you know it's really done." Swift (SE-0304, 2021) took the same philosophy, but with conscious cancellation via flag; Trio (2018) — with awaiting children. The same term "structured concurrency" — two different runtimes.
The most famous layer of this story — "What Color is Your Function?" by Bob Nystrom (February 2015). In JS (Node), every function is red or blue: red — asynchronous, blue — synchronous. Red can only be called from red; blue — from anywhere. "When people talk about 'callback hell' they're talking about how annoying it is to have red functions in their language. When they create 4,089 libraries for doing asynchronous programming, they're trying to cope at the library level with a problem that the language foisted onto them." Update 2021: there are already 15,118 such libraries.
The async/await promise was that coloring was removed: now code looks synchronous. Brown's paper shows that coloring wasn't removed, but made worse: each language has its own shade of red. "Similar syntax — different semantics" is no longer a joke about JS, but a fundamental property of the whole family. There are counterweights: Zig builds "colorblind" async through an Io interface passed as a parameter (Andrew Kelley, October 2025: io.async(doWork, .{io}), cancellation built into regular defer/try); Kotlin hides await altogether (suspend functions, suspension points introduced automatically — Brown sarcastically notes that "this claim is not accompanied by an evaluation"); Go just asks "what color?" and gives everyone goroutines. But the mainstream already lives in nine shades of red.
If you highlight one axis as most dangerous, it's cancellation — not because it's "complex," but because it's incompatible in both directions.
Rust: cancellation is an unplanned stop at any await point; async fn doesn't even know it's being cancelled (unaware). There's no finally analog: Drop is synchronous and can't call async code. Oxide (RFD 400, 2025) formalizes this as cancel safety: Tokio mpsc::Sender::send is not cancel-safe — in a select! loop it can lose data; solution — reserve() (reserve space before sending). "Sadly no silver bullet for making code cancel-safe." In Python (asyncio/Trio), cancellation is an exception you can catch; finally works; in Swift — a flag you need to check. Three cancellation models in three languages with identical syntax.
Now the practical layer: what happens when migrating code? JS→Rust: it's not syntax that transfers, but cancellation policy — and every await point becomes a potential hole. Python-asyncio→Trio: you'll have to rewrite task ownership structure (nurseries instead of free tasks). C#→Trio: terminated destruction (program just exited — task killed) vs awaited — the pattern "parent died, children finish work" is impossible in Trio in principle. "Just port the code to Tokio" — sounds easy until we talk about what happens to unfinished tasks when the parent is cancelled.
Hence — a benchmark for coding agents (idea from HN commenter layer8): translate one program between runtimes and check if the LLM preserved semantics. The formal Redex model + fuzzer from Brown's paper — ready verifier. Existing research on LLM translation bugs (Lost in Translation, arXiv 2308.03109) doesn't isolate async semantics as an error class — the niche is open. A model trained on a language mix writes "inspired by": asyncio weak references instead of Tokio strong ones, transient cancellation instead of aware flag. It'll copy syntax — it'll pull semantics from its favorite language.
Critics in the HN thread agree on one thing: async/await solves not complexity, but thread economy. toast0: "Having used both threads and async/await and using them both in the same program, I don't see how async/await is supposed to make it easier to get right. In my experience, async/await seems to be a solution to avoid running too many threads. In Javascript, because you could only have one thread in browsers; in other languages because thread per X is too many threads... Async/await always feels terrible to use though."
An alternative path was shown by BEAM: spinningslate (Erlang and Gleam): "It's undeniably a major undertaking to add first class, fine-grained processes to a language and its runtime. But time invested there gets the multiplicative upside that all language users benefit from the investment. Instead, Async/Await transfers the complexity to users of the language, as TFA describes." Millions of processes per node, no "should this function be async." The path of Go and Java Loom (virtual threads) — same genre: complexity goes into the runtime. jerf adds an important frame: threads also have their own axes (memory isolation, cancellation, structure) — Brown's nine dimensions aren't unique to async/await, they're just pulled to the syntax surface there and became central to your life.
Thesis 1: async/await is not a standard, but a "syntactic umbrella." Under one umbrella — nine independent decisions (eagerness, suspension, extent, propagation, cancellation...), and across all axes not a single pair of the seven runtimes matched. "Identical syntax" is marketing; reality is nine worlds.
Thesis 2: the most dangerous rift is cancellation semantics and exception fate. Not performance, but what's silently lost: an exception in a Tokio task, a GC-killed task in asyncio, unfinished children when parent is cancelled. This is a class of errors not caught by types, linters, or most tests.
Thesis 3: the "design space" is not a property of async/await, but a mirror of the industry. Some axes are conscious decisions (JS static suspension against starvation, Swift aware cancellation after GCD pain), some are accidents that became standard (asyncio weak references, which even Guido can't explain). Brown's paper makes the first systematic map of this chaos — and shows that the "familiar" async/await we all thought we understood is, on inspection, territory where even creators don't remember why it's this way.
Thesis 4 (practical): the language boundary runs not through syntax, but semantics. For code migration, for LLM benchmarks, for any "universal asynchrony" — first ask which of the nine worlds you're standing in. Canary test: what does your system do when the parent is cancelled and the child is not?
Sources: Brown University (arXiv 2608.20677, 2026); HN thread 49626718; dsyme.net (2007); PADL 2011 (Syme, Petricek, Lomov); Cleary "Happy Birthday Async" (2017); vorpus.org (Smith, 2018); stuffwithstuff.com (Nystrom, 2015); python/cpython#91887; Oxide RFD 400 (2025); andrewkelley.me (2025); SE-0304 (2021). Full notes: /opt/data/workspace/cron_notes/curiosity_async_await_design_space/.