When you write await in one language and then write it in another, are you actually expressing the same idea? I assumed yes for years. I built bots in Python, ported chunks to JavaScript, prototyped a worker in Rust, and treated async/await as a portable skill, like knowing how a for-loop behaves. A paper making the rounds this fall suggests that assumption is doing a lot of unearned work.
The paper is “A Design Space Exploration of Async/Await” by Gavin Gray, Shriram Krishnamurthi, and Will Crichton, out of Brown’s Cognitive Engineering Lab. It landed on arXiv on August 21, 2026, is slated for OOPSLA 2026, and the authors posted about it on September 8. It surfaced on Hacker News with a modest score, which is often how the most useful papers arrive: quietly, while the flashier posts get the traffic.
The core contribution is a design space with nine dimensions. Those dimensions describe how tasks behave and execute, and the paper points out that languages make different choices along them. Different choices produce different task lifecycles and different cancellation semantics. That is the part that should get your attention if you build bots for a living.
Straight-line asynchrony is a promise, not a guarantee
The appeal of async/await has always been that concurrent code reads top to bottom. You write a sequence of steps, sprinkle in await where you touch the network, and the shape of the function still looks like the shape of your intent. The paper’s framing calls this straight-line asynchrony, and the goal of the whole paradigm is to make concurrent programming simpler to reason about.
The paper’s own illustrative snippet is about as plain as it gets: an async function that prints “A”, awaits a two-second sleep standing in for a log write, then prints “B”. Nothing surprising in isolation. The surprise is what happens around it. When does that function actually start running? Who owns it once it is running? What happens if the thing that called it walks away? Those questions do not have one answer across languages, and the code on your screen does not tell you which answer you are getting.
Why bot builders get burned here specifically
Bots are a concurrency stress test disguised as a chat interface. A single conversational turn might fan out to a model call, a vector search, a couple of tool invocations, and a logging write nobody is waiting on. Then the user sends a new message and cancels the whole thing mid-flight, or a timeout fires, or a rate limiter tells you to back off.
Every one of those situations is a question about task lifecycle and cancellation, which is precisely where the paper says languages diverge. In my own work, the bugs that took longest to find were never the model prompts. They were things like:
- A background task that was created but never actually driven forward, so a log line just never appeared.
- Cleanup code after an
awaitthat did not run because the task was cancelled at exactly that point. - A “fire and forget” call that turned out to be forgotten in the literal sense, garbage collected before it did anything.
- A cancellation that stopped the outer request but left three inner calls burning tokens.
None of those are exotic. They are the ordinary cost of assuming that await means the same thing everywhere.
What a design space is actually good for
The value of naming nine dimensions is not that you will memorize them. It is that a design space gives you a checklist for a language you have never used in anger. Instead of “does this language have async,” you get to ask sharper questions about how tasks start, how they finish, who can stop them, and what happens to the work in between.
That reframing is useful even inside one language. Most of us learned async by imitation, copying patterns from docs and Stack Overflow until the tests went green. That works right up until you need to explain, precisely, why a cancelled task left a half-written record in your database. A vocabulary for the choices your runtime made is how you move from pattern matching to actual understanding.
The practical takeaway
If you maintain a bot that spans more than one runtime, and plenty do now, treat cancellation as a design decision rather than an afterthought. Write a small test that cancels a task mid-await and assert what survives. Do it once per language you ship in. The results will probably not match your intuition, and finding that out in a test file beats finding it out in production traffic.
I like this paper because it does not sell a new framework. It gives names to differences we have all tripped over and pretended were our own fault. The keyword looks identical across languages. The semantics underneath are a design space, and knowing that changes how carefully you read the docs.
đź•’ Published: