Over the last decade, async/await won the concurrency wars because it is exceptionally easy. It allows developers to write asynchronous code that looks virtually identical to synchronous code.
But beneath that familiar syntax lies massive structural complexity. It hides control flow, obscures hardware realities, and ultimately pushes the burden of scheduling back onto the developer.
Rich Hickey articulated this perfectly in his talk Simple Made Easy: “Easy” is what is familiar and close at hand, while “Simple” is what is structurally untangled1. async/await is easy to write, but it is fiercely complex to operate.
Rob Pike talked about this architectural shift during his 2023 GopherConAU address:
Compared to goroutines, channels and select, async/await is easier and smaller for language implementers to build… But it pushes some of the complexity back on the programmer, often resulting in what Bob Nystrom has called ‘colored functions’. […] It’s important, though, whatever concurrency model you do provide, you do it exactly once, because an environment providing multiple concurrency implementations can be problematic.2
Pike’s remark about “multiple concurrency implementations” and async/await is exactly what is failing in production today.
The Production Trap: Confusing Asynchrony with Concurrency
The fundamental trap of async/await is that it conflates asynchrony (yielding while waiting for I/O) with concurrency (dealing with multiple things at once).
The syntax is a trap because it disguises interleaved state machines as isolated, sequential threads. Lulled by this illusion, a developer writes an async function exactly as they would blocking code — fetching a database record over the network, then immediately crunching the data. But what happens when that data crunching involves parsing a 10MB JSON payload, traversing a massive collection, or executing a compute-heavy cryptographic proof?
The cooperative executor halts.
... continue reading