Async/await: syntax over a state machine
async and await get read as a spell for making slow code fast. They do nothing of the kind. The pair is notation - a way to write a function that can stop in the middle, hand its thread to someone else, and pick up later exactly where it left off. No extra speed, no extra cores. What you get is the ability to have one thread be partway through thousands of operations at once, as long as almost all of them are waiting.
What await actually means
await reads like “block here until this is done.” It means close to the opposite. It marks a yield point: “I need a result that isn’t ready, so suspend me here, remember where I was, and give the thread back to the scheduler so other work can run. Wake me when the result arrives.” The waiting still happens. What changes is that the thread does not sit on it. A blocking call owns its thread through the whole wait; an await releases it.
The function becomes a state machine
For a function to pause and resume, its progress has to become data. That is what the compiler does to an async function: it rewrites it into an object - a state machine - that remembers which point it reached and holds the local variables that must survive the wait.
Each await becomes a labeled resume point, and everything between two of them is one state. The locals that are still needed after a suspension stop being stack variables and become fields on that object, because the stack frame is long gone by the time the function wakes back up.
async function loadUser(id) {
const row = await db.query(id); // resume point 1
const avatar = await fetch(row.avatarUrl); // resume point 2
return render(row, avatar);
}
Under the hood that becomes roughly: run until the first await, stash id and a “state = 1” marker, return control. When the query completes, re-enter at state 1 with row in hand, run to the next await, park again. The linear-looking function is really three fragments the runtime stitches together over time.
stateDiagram-v2 [*] --> Running Running --> Suspended: hit await, save state, release thread Suspended --> Running: awaited result ready Running --> [*]: return
Who resumes it: the event loop
A suspended task has to be woken by something, and that something is the runtime’s event loop. When you await an IO operation, the runtime registers interest in its completion and parks your state machine, then goes on running other ready tasks. When the OS signals the IO is done, the runtime drops your task’s continuation onto the loop’s ready queue, and it resumes on a later turn.
This is the same cooperative yielding coroutines do (see Threads vs coroutines: who decides to yield); await is just the yield point spelled out in the language. And because it is cooperative, the same hazard applies: a task that never awaits - a tight compute loop - never yields, and freezes the loop for everyone on it. Async is for overlapping waiting, not for computing (see CPU-bound vs IO-bound).
The catch: async is a color
There is a tax, and it is structural rather than about cycles. To await something, the function has to be async. To get the result of an async function, the caller has to await it, so the caller has to be async too. The property climbs the call graph: async is contagious. This is the “function coloring” problem - a codebase splits into sync functions and async functions, and the two do not compose freely. A sync function cannot simply call an async one and wait on it; it needs an explicit bridge back to the event loop. Much of the friction of async code is not the runtime cost but this: the color leaks into everything it touches.
The bottom line
await is cooperative yielding with good manners and clean syntax. It adds no parallelism and makes no single operation faster - it stops one thread from wasting itself on waits. When the work is thousands of concurrent IO operations, that is transformative. When the work is one CPU-bound computation, it is pure overhead, which is its own note: The cost of async.
Related
- Threads vs coroutines: who decides to yield - await is a coroutine yield point named in the language
- The cost of async - what this convenience costs, and when to skip it
- Queues Are Everywhere - the event loop your resumed task lands back on
- CPU-bound vs IO-bound - async helps the waiting kind of work, not the computing kind
- How Programs Actually Run MOC - the map these runtime notes hang from