If the speed came at the cost of readable code, must you give one of them up? Keep the way the work is done and rewrite only the log, so it reads top to bottom again.
See this first in FoundationsWhen does an event loop win, and when does it lose?Callbacks work but nest badly: query, then query again with the result, then undo on failure, and error handling scatters. Coroutines fix this with syntax — the execution model stays the event loop, only the shape of the code returns to looking synchronous.
At an await point exactly what chapter 11 did happens: register a continuation and return. The difference is that the compiler or runtime writes that for you, and execution resumes at the same spot when the IO completes.
An ordinary function runs to its return. A coroutine can pause mid-way and continue later from that point, with its locals and position kept somewhere — the same idea as chapter 6's context saving, but handled far more cheaply by the language runtime.
Few. A coroutine is not a stack but roughly "a paused position plus some locals", so tens of thousands of coroutines cost incomparably less memory than tens of thousands of threads.
async/await in JavaScript and Python, suspend functions in Kotlin, goroutines in Go. Notice the animation is identical to chapter 11 — the execution model did not change, only the syntax.
Coroutines are not a cure-all. Three cases are actively worse. First, long CPU work: with no suspension point, no other coroutine can get in. Second, calling a blocking library. Third, purely sequential logic with nothing to overlap. There you gain nothing and pay in broken stack traces and harder debugging.
An async function can only be awaited naturally from another async function, so turning one place asynchronous spreads through the whole call chain. This is the function colour problem. It is precisely why virtual threads drew attention — they deliver the same effect without colouring your code.
1async handle(req) {2 id = parse(req)3 user = await db.query(id)await = register + return, resumes here4 return toJson(user)5}
Try it in the simulator
Terms in this chapter
Why it evolved this way
People reason far better about top-to-bottom code. If performance demands unreadable code, teams ship bugs — so languages adopted coroutines: event-loop performance with synchronous readability.
Reads like sync, runs like an event loop. Sugar, not magic.
The remaining problem
One weakness remains: the loop assumes everyone yields quickly. What if code does not? →