It's long been clear that there were fundamental implementation choices that mattered between async runtimes, but _nine_ design dimensions? Damn.
I think async is deceptive in that it seems like a self-contained and relatively straightforward aspect of a language. But there are many design choices to be made and they all have wide implications.
Plus I think the implications of many of these dimensions are not fully understood and that collectively we are still trying to understand how they are playing out in implementations. Add to this the subtle nature of some of the implications plus the combinations...
I think a good comparison is lexical vs dynamic scope in programming languages. This is a design dimension that was argued over for a decade or two in the early years of programming language design. It was only as time went by, and experience gained by working with concrete implementations that it became clear that lexical scoping should be the default choice and dynamic scoping should be restricted to various niches.
With these dimensions of design variance defined, you could also make a closeness measure in 9-dimensional space and identify the most or least similar combos.
Also a great teaching tool, if someone knows one async system, to be able to show them the differences on each axis from their prior one to a new one they’re learning.
Amazing work. It's one thing to say "async is complex". It's another to parse that statement so carefully as to have a cross-language theory of async execution. Looking forward to digging into this!
It would be a fun coding agent benchmark to have them translate such a program between the different languages and see whether they preserve the respective semantics.
I answered the quiz and it said "you must be a Javascript developer", which is true enough - that's probably my second-most-proficient language. In fact, I'm a Ruby developer partially because I hate the idea of async/await and I'm feeling very smug about my choice after reading this.
Some of these design decisions seem indefensible to me. For example, what the authors call "Suspension":
-> Static: Await points guaranteed to suspend -- JavaScript
-> Dynamic: No guarantees on awaiting tasks -- C# · Swift · Tokio · Smol · Asyncio · Trio
What is "await" if not a synonym for "suspend"?!?
async/await is one product of a long line of thought that says "threads are too hard for programmers to get right". Threads (really, shared memory) have real usability issues for developers, but once you grok the semantics (which largely map to the physical execution model in a CPU) that knowledge is transferrable across virtually all languages and runtimes.
> async/await is one product of a long line of thought that says "threads are too hard for programmers to get right".
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 and queuing to a thread pool might not be desirable either.
Async/await always feels terrible to use though. Some other way to get thread like semantics without having to have OS threads for everything seems better (to me). Erlang processes, Java Loom Virtual Threads (which I haven't used), etc. If it avoids having all memory shared, even better.
> What is "await" if not a synonym for "suspend"?!?
There are scenarios where something might need to await and might not. Why take the hit if you are able to do something synchronously? Edit: this is especially important given the “viral” nature of colored functions.
It does make it hard to reason about, but this kind of problem is all over the place - e.g. very similar-looking code can have very different semantics depending on your framework if you’re using jsx or a particular decorator means one thing in one project and something else in another. That’s just part of the game at this point.
C# has Task.FromResult(), which is useful if you are implementing an interface that allows async work but your implementation doesn't require it. I believe the runtime will check for this case and continue execution. It's better for the cache to keep executing the current task on the current thread.
I don't really understand gp's point. From inside the code, you can't tell if there was a pause or not. Clock time or thread id are heuristics, but you can't really be sure.
I'm teaching async calls in makearcade to my 10yo son, to bypass a platform bug. He said he doesn't get it. My answer was: Don't worry, adults don't get it either.
It's long been clear that there were fundamental implementation choices that mattered between async runtimes, but _nine_ design dimensions? Damn.
I think async is deceptive in that it seems like a self-contained and relatively straightforward aspect of a language. But there are many design choices to be made and they all have wide implications.
Plus I think the implications of many of these dimensions are not fully understood and that collectively we are still trying to understand how they are playing out in implementations. Add to this the subtle nature of some of the implications plus the combinations...
I think a good comparison is lexical vs dynamic scope in programming languages. This is a design dimension that was argued over for a decade or two in the early years of programming language design. It was only as time went by, and experience gained by working with concrete implementations that it became clear that lexical scoping should be the default choice and dynamic scoping should be restricted to various niches.
C# is my primary, but the quiz tells me I'm a JS dev.
Feel like I should assign myself a couple dozen Jon Skeet posts to read now, to make up for this embarrassment.
At last someone took the time to pore over all the tedious crap that I have been trying and failing to keep straight in my head since forever.
The paper explores how async/await behaves across today's languages: https://arxiv.org/abs/2608.20677
With these dimensions of design variance defined, you could also make a closeness measure in 9-dimensional space and identify the most or least similar combos.
Also a great teaching tool, if someone knows one async system, to be able to show them the differences on each axis from their prior one to a new one they’re learning.
Amazing work. It's one thing to say "async is complex". It's another to parse that statement so carefully as to have a cross-language theory of async execution. Looking forward to digging into this!
It would be a fun coding agent benchmark to have them translate such a program between the different languages and see whether they preserve the respective semantics.
or, Java Virtual Threads and chill.
No. Because concurrency vs parallelism
>You must be a C#, Swift, Asyncio, or Tokio developer — hard to narrow down, you all agree on this one.
I think that's definitely right. Knowing the semantics of the language you mainly use is important.
Beautiful
Great work. Must read for anyone working with this type of concurrency.
I answered the quiz and it said "you must be a Javascript developer", which is true enough - that's probably my second-most-proficient language. In fact, I'm a Ruby developer partially because I hate the idea of async/await and I'm feeling very smug about my choice after reading this.
Some of these design decisions seem indefensible to me. For example, what the authors call "Suspension":
-> Static: Await points guaranteed to suspend -- JavaScript
-> Dynamic: No guarantees on awaiting tasks -- C# · Swift · Tokio · Smol · Asyncio · Trio
What is "await" if not a synonym for "suspend"?!?
async/await is one product of a long line of thought that says "threads are too hard for programmers to get right". Threads (really, shared memory) have real usability issues for developers, but once you grok the semantics (which largely map to the physical execution model in a CPU) that knowledge is transferrable across virtually all languages and runtimes.
> async/await is one product of a long line of thought that says "threads are too hard for programmers to get right".
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 and queuing to a thread pool might not be desirable either.
Async/await always feels terrible to use though. Some other way to get thread like semantics without having to have OS threads for everything seems better (to me). Erlang processes, Java Loom Virtual Threads (which I haven't used), etc. If it avoids having all memory shared, even better.
> What is "await" if not a synonym for "suspend"?!?
There are scenarios where something might need to await and might not. Why take the hit if you are able to do something synchronously? Edit: this is especially important given the “viral” nature of colored functions.
It does make it hard to reason about, but this kind of problem is all over the place - e.g. very similar-looking code can have very different semantics depending on your framework if you’re using jsx or a particular decorator means one thing in one project and something else in another. That’s just part of the game at this point.
C# has Task.FromResult(), which is useful if you are implementing an interface that allows async work but your implementation doesn't require it. I believe the runtime will check for this case and continue execution. It's better for the cache to keep executing the current task on the current thread.
I don't really understand gp's point. From inside the code, you can't tell if there was a pause or not. Clock time or thread id are heuristics, but you can't really be sure.
I'm teaching async calls in makearcade to my 10yo son, to bypass a platform bug. He said he doesn't get it. My answer was: Don't worry, adults don't get it either.
> What is "await" if not a synonym for "suspend"?!?
it's a question of whether the runtime is guaranteed to suspend at an await point or if it may choose not to
> async/await is one product of a long line of thought that says "threads are too hard for programmers to get right".
what? no! concurrency vs parallelism etc etc