Coroutines — suspendable functions, not "async functions"
Before C++20 you had two crude options for asynchronous code: callbacks (callback hell, inverted control flow) or threads (expensive, requires synchronization). Coroutines are a third path — a function that can suspend at an arbitrary point and resume later without occupying a system thread while it waits.
The core idea is not "async function" — it is suspendable function. A coroutine does not run in the background by itself; its state lives on the heap as a coroutine frame, and who calls resume() and when is decided by an external scheduler, a generator driver, or an I/O event. Async I/O, generators, lazy sequences, and cooperative multitasking are all separate use cases built on top of the same suspension primitive.
C++20 only ships the bones of the mechanism: keywords, types (coroutine_handle, suspend_always/never), and the requirements for promise_type and Awaitable. The coroutine type itself (Task<T>, Generator<T>, Lazy<T>…) is written by the user or by a library (cppcoro, Asio, std::execution). The full layered map is below.
Topic map
- What is a coroutine — a function with suspension points, lazy semantics, how it differs from a regular function.
- co_yield, co_await, co_return — the three keywords and what they mean semantically.
- Coroutine frame — where state lives, heap allocation, HALO optimization, common traps.
- std::coroutine_handle —
resume/destroy/done,from_promise, ownership and lifetime. - promise_type — customization points:
get_return_object,initial_suspend,final_suspend,return_value,unhandled_exception. - Awaitable protocol — three methods (
await_ready/await_suspend/await_resume) and howco_awaitunrolls into them. - Generators —
co_yield-driven sequences,std::generator<T>from C++23, lazy ranges. - Symmetric transfer — returning
coroutine_handle<>fromawait_suspend, tail-call resume, and why it saves the stack.
Common traps
| Mistake | Consequence |
|---|---|
| Capturing a reference to a caller's parameter and continuing past a suspend point | Dangling reference in the frame — UB after the caller returns |
Losing the coroutine_handle of an unfinished coroutine | Memory leak: nobody frees the frame on the heap |
Writing return instead of co_return in a coroutine body | Compile error — only co_return is allowed in a coroutine |
Throwing from final_suspend() | UB — the standard requires it to be noexcept |
Direct handle.resume() inside await_suspend when chaining coroutines | Stack overflow on long chains; you need symmetric transfer |
initial_suspend = suspend_never while expecting to collect the result | The body already ran before the object was returned — too late to subscribe |
co_await on an object missing one of the three Awaitable methods | Compile error at instantiation that is not always readable |
| Treating a coroutine as an "async function" with no scheduler | It suspends and nobody ever resumes it |
Saving coroutine_handle::address() after the frame was destroyed | Dangling handle — resume() is UB |
Capturing *this from a method coroutine, then letting the object die before resume | The frame references a destroyed object |
Interview relevance
Coroutines are a relatively fresh topic (C++20) and in 2024–2025 they show up more and more in interviews at companies with mature C++ stacks (game dev, HFT, networked services). Most candidates know the syntax but not the mechanism.
Typical checks:
- Understanding of the coroutine frame — where it lives, who controls its lifetime, when allocation can be elided (HALO).
- The difference between
co_await,co_yield, andco_return— semantically, not syntactically. promise_type— which methods are mandatory, whatinitial_suspendandfinal_suspendactually do.- The Awaitable concept — three methods and three possible return types of
await_suspend(void,bool,coroutine_handle<>). - How coroutines differ from threads and why they do not run "in the background" automatically.
- What symmetric transfer is and what problem it solves.
- Writing a minimal generator in C++20 without
std::generator— a classic live-coding prompt.
Common wrong answer: "Coroutines are async functions, like async/await in C#." That is the opening for a discussion that a coroutine is a suspendable function, and async I/O is only one of many use cases — generators, state machines, and cooperative multitasking sit on the same primitive. The keyword is suspension, not async.