Coroutines
C++20 coroutines — co_await/co_yield/co_return, the coroutine frame, promise_type, awaitables, generators, and symmetric transfer.
30 questions
JuniorTheoryVery commonWhat is std::coroutine_handle and what can you do with it?
What is std::coroutine_handle and what can you do with it?
std::coroutine_handle is a thin non-owning wrapper around a pointer to the coroutine frame. With it you can resume() a coroutine, query done(), destroy() the frame, and reach the promise(). It does not manage the frame's lifetime.
Common mistakes
- ✗Assuming the handle owns the frame and frees it on destruction
- ✗Forgetting to call
destroy(), leaking the frame of an unfinished coroutine - ✗Confusing
done()(reached final suspend) with the coroutine being destroyed
Follow-up questions
- →How do you wrap a
coroutine_handleso the frame is freed via RAII? - →What is the difference between
coroutine_handle<Promise>andcoroutine_handle<>?
JuniorTheoryVery commonWhat do co_await, co_yield, and co_return each do in a coroutine?
What do co_await, co_yield, and co_return each do in a coroutine?
co_await expr suspends until expr is ready, then yields its result. co_yield v suspends and hands v to the caller. co_return finishes the coroutine, optionally passing a result via the promise_type. Any of the three makes the function a coroutine.
Common mistakes
- ✗Believing
co_yieldends the coroutine the same wayco_returndoes - ✗Thinking
co_awaitalways blocks a thread instead of suspending the coroutine - ✗Assuming a function with these keywords still behaves like an ordinary function
Follow-up questions
- →Why is a plain
returnforbidden inside a coroutine? - →What protocol must the operand of
co_awaitsatisfy?
JuniorTheoryVery commonWhat is the promise_type and why does every coroutine need one?
What is the promise_type and why does every coroutine need one?
The promise_type is a compiler-found type that customizes a coroutine. It builds the return object, decides start/end suspension, handles co_yield/co_return values, and catches exceptions. Without it the lifecycle hooks cannot be generated.
Common mistakes
- ✗Confusing the
promise_typewith the caller-facing return object it creates - ✗Thinking the
promise_typeis optional or auto-generated by the compiler - ✗Believing the
promise_typeschedules or runs the coroutine on a thread
Follow-up questions
- →Which member functions must a
promise_typeprovide? - →How does the compiler locate the
promise_typefor a given coroutine?
JuniorTheoryVery commonWhat does it mean for a coroutine to "suspend"?
What does it mean for a coroutine to "suspend"?
Suspension means the coroutine stops at a defined point, saves its local state and resume position into the frame, and returns control to the caller — without unwinding or freeing the frame. It can later be resumed from that point.
Common mistakes
- ✗Confusing coroutine suspension with an OS thread being blocked or descheduled
- ✗Thinking suspension destroys locals or unwinds the stack the way a return does
- ✗Assuming a suspended coroutine continues running somewhere in the background
Follow-up questions
- →What exactly is saved into the coroutine frame at a suspension point?
- →Who is responsible for resuming a suspended coroutine?
JuniorTheoryVery commonHow do coroutines differ from threads?
How do coroutines differ from threads?
A thread is an OS-scheduled, preemptive execution context with its own stack that runs in parallel on another core. A coroutine is a suspendable function with a heap frame, cooperatively scheduled; alone it gives no parallelism.
Common mistakes
- ✗Believing coroutines give parallelism without an external scheduler or thread pool
- ✗Thinking each coroutine occupies its own OS thread
- ✗Assuming
co_awaitautomatically moves work onto a background thread
Follow-up questions
- →When would you choose a coroutine over a thread, and vice versa?
- →How can a coroutine still end up running on multiple threads over time?
JuniorTheoryCommonWhere is a coroutine's state stored while it is suspended?
Where is a coroutine's state stored while it is suspended?
It lives in the coroutine frame — a compiler-generated object normally allocated on the heap. The frame holds the promise_type, the resume point, parameters, and any locals that survive a suspension. The caller's stack frame is gone after the first suspension.
Common mistakes
- ✗Believing coroutine locals live on the regular call stack across suspensions
- ✗Thinking the
coroutine_handlestores the state rather than just pointing to the frame - ✗Assuming the frame is freed automatically the moment the coroutine suspends
Follow-up questions
- →When is the coroutine frame allocated and when is it freed?
- →Can the heap allocation of the frame ever be elided?
JuniorTheoryCommonWhat is a generator, and which coroutine keyword produces one?
What is a generator, and which coroutine keyword produces one?
A generator is a coroutine that produces a sequence of values lazily, one per resume, instead of computing them all upfront. It is built with co_yield, which suspends and hands the current value to the caller. C++23 adds std::generator<T>.
Common mistakes
- ✗Thinking a generator computes all values eagerly rather than on demand
- ✗Associating generators with
co_awaitinstead ofco_yield - ✗Assuming a generator runs its producer on a separate thread
Follow-up questions
- →What does
promise_type::yield_valuedo for a generator? - →Why do generators usually use
suspend_alwaysforinitial_suspend?
JuniorTheoryCommonWhy can't you use a plain return statement inside a coroutine?
Why can't you use a plain return statement inside a coroutine?
A coroutine does not return a value the ordinary way — its result must flow through the promise_type via return_value/return_void. Plain return bypasses that machinery, so the language forbids it; use co_return, which lowers into the promise call.
Common mistakes
- ✗Thinking
co_returnandreturnare interchangeable inside a coroutine - ✗Not realizing the result must pass through
promise_typemember functions - ✗Assuming a function with
co_awaitcan still mix in a plainreturn
Follow-up questions
- →When does
co_returncallreturn_valueversusreturn_void? - →What happens after
co_returnruns — when is the frame destroyed?
JuniorTheoryCommonWhat is the difference between std::suspend_always and std::suspend_never?
What is the difference between std::suspend_always and std::suspend_never?
Both are trivial standard awaitables. For std::suspend_always, await_ready() returns false, so co_await always suspends. For std::suspend_never, await_ready() returns true, so co_await never suspends. Neither carries a result.
Common mistakes
- ✗Thinking
suspend_neverblocks or yields the thread instead of just not suspending - ✗Believing the choice affects every statement rather than one
co_awaitpoint - ✗Confusing the two when picking
initial_suspendfor eager vs lazy start
Follow-up questions
- →How does
initial_suspendreturning each one change when the coroutine starts? - →What three methods must any awaitable, including these, provide?
MiddleTheoryCommonWhat three calls does the compiler generate for a co_await expr?
What three calls does the compiler generate for a co_await expr?
The compiler emits three Awaitable calls: await_ready() decides whether suspension is needed; await_suspend(handle) runs while the frame is parked; await_resume() yields the expression's value on resume.
Common mistakes
- ✗Believing
await_suspendalways suspends — whenawait_ready()returnstruesuspension is skipped entirely - ✗Thinking
await_resume()is called only on a separate thread rather than on whichever thread resumes the frame - ✗Assuming the value of a
co_awaitexpression comes fromexpritself, not fromawait_resume()
Follow-up questions
- →What does the return type of
await_suspendchange about resumption? - →How does
operator co_awaitorawait_transformfit into this lowering?
MiddleTheoryCommonDoes co_await move the coroutine's work to another thread?
Does co_await move the coroutine's work to another thread?
No. co_await only suspends the coroutine and returns control to the caller — it never spawns a thread. Resumption runs on whatever thread calls handle.resume(). Switching threads needs a scheduling Awaitable.
Common mistakes
- ✗Believing coroutines run concurrently in the background without any external scheduler
- ✗Calling coroutines an 'async' feature and assuming parallelism comes for free
- ✗Expecting
co_awaitto offload CPU-bound work off the current thread
Follow-up questions
- →What kind of Awaitable would actually resume the coroutine on a thread pool?
- →Why is suspension, not asynchrony, the defining property of a coroutine?
MiddleTheoryCommonShould a coroutine start running on call or only on first resume?
Should a coroutine start running on call or only on first resume?
It is your choice, set by initial_suspend. suspend_never makes the coroutine eager — it runs to the first suspension on call. suspend_always makes it lazy — it starts on the first resume(). Generators are usually lazy.
Common mistakes
- ✗Thinking the standard fixes start timing instead of leaving it to
initial_suspend - ✗Assuming the frame is not allocated until the first
resume() - ✗Confusing
initial_suspendwithfinal_suspendwhen reasoning about start vs end
Follow-up questions
- →What problems can an eagerly-started coroutine cause for error handling?
- →Why is lazy start the natural choice for a generator?
MiddleTheoryCommonWhat does final_suspend control, and why is it usually suspend_always?
What does final_suspend control, and why is it usually suspend_always?
final_suspend() runs after the body and decides whether the frame stays alive. suspend_always keeps it so the caller can still read the result or exception; suspend_never destroys it at once, dangling the handle.
Common mistakes
- ✗Confusing
final_suspendwithinitial_suspend— one runs after the body, the other before it - ✗Returning
suspend_neverand then reading the result through a now-destroyed frame - ✗Forgetting
final_suspend()must be declarednoexceptor the program is ill-formed
Follow-up questions
- →Who is responsible for destroying the frame when
final_suspendreturnssuspend_always? - →How can
final_suspenduse symmetric transfer to resume a waiting coroutine?
MiddleCodeCommonHow do you implement a minimal generator without std::generator?
How do you implement a minimal generator without std::generator?
Define a type with a nested promise_type whose yield_value stores the value and returns suspend_always. Wrap a coroutine_handle, expose next()/value(), and destroy() the frame in the destructor.
Common mistakes
- ✗Returning
suspend_neverfromyield_value, so the coroutine never actually pauses onco_yield - ✗Forgetting
destroy()in the destructor, leaking the heap frame - ✗Omitting
noexceptonfinal_suspend(), making the program ill-formed
Follow-up questions
- →How would you give the generator real iterators usable in a range-based
for? - →What does
std::generatoradd over this hand-rolled version in C++23?
MiddleTheoryCommonWhich methods must a promise_type provide?
Which methods must a promise_type provide?
It must provide get_return_object, initial_suspend, final_suspend (noexcept), unhandled_exception, and exactly one of return_value or return_void. A generator also needs yield_value. Missing any required member is ill-formed.
Common mistakes
- ✗Forgetting that
final_suspendmust be declarednoexcept - ✗Defining both
return_valueandreturn_voidin the same promise - ✗Confusing the promise's required methods with the awaitable's three methods
Follow-up questions
- →Why must
final_suspendbenoexcept? - →What goes wrong if you define both
return_valueandreturn_void?
MiddleTheoryCommonSemantically, how does co_yield differ from co_return?
Semantically, how does co_yield differ from co_return?
co_yield v calls promise.yield_value(v) and suspends the coroutine — it can be resumed again, producing a sequence. co_return calls return_value/return_void and finishes the coroutine — no further resumption is possible.
Common mistakes
- ✗Thinking a coroutine can be resumed after
co_return— it cannot, the body is done - ✗Believing
co_yieldfinishes the coroutine like a normalreturnstatement - ✗Assuming
co_yield xis just sugar forco_return xinside a loop
Follow-up questions
- →What awaitable does
yield_valuereturn and why is it usuallysuspend_always? - →Can a single coroutine use both
co_yieldandco_return?
SeniorDesignCommonYou are choosing how to structure asynchronous work in a C++ service. Compare four approaches — plain callbacks, std::future, C++20 coroutines, and the C++26 structured-concurrency model senders/receivers — in terms of readability, composition of dependent steps, and error propagation.
You are choosing how to structure asynchronous work in a C++ service. Compare four approaches — plain callbacks, std::future, C++20 coroutines, and the C++26 structured-concurrency model senders/receivers — in terms of readability, composition of dependent steps, and error propagation.
Callbacks are simple but nest. Futures look synchronous but compose awkwardly. C++20 coroutines give linear co_await code; C++26 senders/receivers add structured schedulers.
Common mistakes
- ✗Mixing callback and future styles within one library — fragile error handling
- ✗Using
std::asyncdefaults (std::launch::async | std::launch::deferred) and getting unexpected lazy execution - ✗Capturing references in coroutines — dangling across suspension
Follow-up questions
- →Why is
std::futurepoor for composition compared tofolly::Future? - →What does cancellation look like in senders/receivers?
SeniorTheoryCommonWhat turns a regular function into a C++20 coroutine?
What turns a regular function into a C++20 coroutine?
A function becomes a coroutine if its body uses co_await, co_yield, or co_return. The compiler transforms it into a state machine with a heap-allocated frame. The return type must define a promise_type.
Common mistakes
- ✗Capturing references in a coroutine — they may dangle across suspension
- ✗Forgetting that the coroutine handle owns the frame — destroying handle leaks if not done correctly
- ✗Confusing
co_yield(suspends with a value) andco_return(terminates the coroutine)
Follow-up questions
- →What does
promise_type::initial_suspendcontrol? - →How does
std::generator(C++23) wrap the coroutine machinery?
SeniorTheoryCommonWhat are C++20 coroutines? co_await, co_yield, co_return.
What are C++20 coroutines? co_await, co_yield, co_return.
C++20 coroutines suspend and resume without blocking a thread. co_await/co_yield/co_return makes a function a coroutine; the compiler turns it into a heap-allocated state machine. The return type needs a user-defined promise.
Common mistakes
- ✗Expecting C++20 to provide ready-made coroutine types — the standard only provides the machinery; you need a promise type, which must be written or taken from a library (C++23 adds
std::generator) - ✗Confusing coroutines with multithreading — coroutines are cooperative and single-threaded by default; parallelism requires explicitly scheduling them on a thread pool
- ✗Dangling references in coroutine frames — if a coroutine captures a local by reference and the caller destroys it before resumption, it's UB; prefer value capture or shared ownership
Follow-up questions
- →What does
co_await std::suspend_always{}do vsco_await std::suspend_never{}? - →How does the promise type control allocation of the coroutine frame (operator new elision)?
MiddlePerformanceOccasionalWhen is the coroutine frame allocated and freed, and can the allocation be elided?
When is the coroutine frame allocated and freed, and can the allocation be elided?
The frame is allocated (normally on the heap) once when the coroutine is first called, and freed when destroy() runs. The compiler may elide the heap allocation (HALO) when the coroutine's lifetime is fully visible to it.
Common mistakes
- ✗Believing the frame is reallocated on each resume rather than once at call
- ✗Thinking the frame frees itself at
co_returninstead of atdestroy() - ✗Assuming HALO always happens rather than only when lifetime is fully visible
Follow-up questions
- →What conditions let the compiler apply HALO and inline the frame?
- →How can you supply a custom allocator for the coroutine frame?
MiddleTheoryOccasionalHow do you obtain a coroutine_handle from inside the promise, and why?
How do you obtain a coroutine_handle from inside the promise, and why?
Call std::coroutine_handle<promise_type>::from_promise(*this) — it rebuilds the handle from the promise's address, since the promise sits at a known frame offset. get_return_object() uses it to build the caller's wrapper.
Common mistakes
- ✗Calling
from_promisewith a promise that is not actually a coroutine-frame member - ✗Confusing
from_promise(promise to handle) withfrom_address(raw pointer to handle) - ✗Thinking the handle is only available to the caller, never to the promise itself
Follow-up questions
- →Why must
from_promiseandpromise()be exact inverses of each other? - →What undefined behaviour arises if you pass a non-frame promise to
from_promise?
MiddleDebuggingOccasionalHow can a coroutine leak memory, and how do you prevent it?
How can a coroutine leak memory, and how do you prevent it?
coroutine_handle does not own the frame. If you lose the handle to an unfinished coroutine, or never call destroy(), the frame leaks. Prevent it by wrapping the handle in a RAII type whose destructor calls destroy(), and follow proper move semantics so ownership is unique.
Common mistakes
- ✗Believing
coroutine_handleowns or reference-counts the frame - ✗Copying a handle so two owners both call
destroy()— a double free - ✗Thinking
final_suspendreturningsuspend_alwayscleans up by itself
Follow-up questions
- →What goes wrong if
final_suspendreturnssuspend_never? - →How do you detect a leaked coroutine frame with a sanitizer?
MiddleCodeOccasionalWhy should coroutine parameters usually be passed by value?
Why should coroutine parameters usually be passed by value?
A by-value parameter is copied into the coroutine frame, so it stays alive across suspensions for the coroutine's whole lifetime. A reference parameter only refers to the caller's object — if the caller returns first, the reference dangles, which is undefined behavior.
Open full question →Common mistakes
- ✗Thinking a reference parameter is copied into the frame like a by-value one
- ✗Assuming the caller's stack frame stays alive until the coroutine finishes
- ✗Believing the compiler rejects or rewrites reference parameters automatically
Follow-up questions
- →Is a captured lambda used as a coroutine safe across suspensions?
- →When is a reference parameter actually acceptable in a coroutine?
MiddleDebuggingOccasionalHow can naive resume() chaining overflow the stack?
How can naive resume() chaining overflow the stack?
If await_suspend calls handle.resume() on the next coroutine, and that one resumes another, each resume() nests inside the previous stack frame. A long chain never unwinds and overflows the stack; heap frames do not bound it.
Common mistakes
- ✗Calling
handle.resume()insideawait_suspendinstead of returning the handle - ✗Thinking heap-allocated frames mean coroutine chains can never overflow the stack
- ✗Believing the danger is only theoretical and ignoring deep producer/consumer chains
Follow-up questions
- →How does returning a
coroutine_handle<>fromawait_suspendbound the stack depth? - →Why is a tail call the key difference between safe and unsafe resumption?
MiddleTheoryOccasionalWhat happens when an exception escapes a coroutine body?
What happens when an exception escapes a coroutine body?
An exception escaping the body is caught by the compiler and routed to promise.unhandled_exception(). It does not reach the resumer's stack. A typical promise stores std::current_exception() and rethrows it later from get().
Common mistakes
- ✗Wrapping
handle.resume()intry/catchexpecting to catch the body's exception there - ✗Forgetting to store the exception in the promise, so
get()silently returns a stale value - ✗Throwing from
unhandled_exception()itself, which leads tostd::terminate
Follow-up questions
- →Why must
unhandled_exception()itself not let an exception escape? - →How does a
Taskrethrow a stored exception to its awaiter?
SeniorTheoryOccasionalWhat do the different return types of await_suspend mean?
What do the different return types of await_suspend mean?
void suspends and returns to the caller. bool does the same when true, but false resumes the current coroutine at once. A returned coroutine_handle<> is symmetric transfer: that handle is resumed without growing the stack.
Common mistakes
- ✗Thinking
await_suspendreturningfalsesomehow cancels the coroutine instead of resuming it - ✗Believing the returned
coroutine_handle<>is just resumed later rather than transferred to immediately - ✗Assuming
voidandbool truediffer in behaviour — both suspend and yield to the caller
Follow-up questions
- →Why is the
coroutine_handle<>overload essential for chained awaiters? - →What happens if
await_suspendresumes a handle and then also returns a handle?
SeniorCodeOccasionalHow do you write a custom Awaitable that resumes a coroutine on I/O completion?
How do you write a custom Awaitable that resumes a coroutine on I/O completion?
Make await_ready() return false, have await_suspend(handle) register the I/O and stash the handle in the completion callback, and let await_resume() return the result. The callback calls handle.resume().
Common mistakes
- ✗Returning
truefromawait_ready(), so the coroutine never suspends to wait for the I/O - ✗Capturing the
handleby reference in the callback — it dangles onceawait_suspendreturns - ✗Resuming the handle before the result is stored, so
await_resume()reads stale data
Follow-up questions
- →Why is it safe to resume the handle from the I/O thread rather than the original one?
- →How would you propagate an I/O error out through
await_resume()?
SeniorDebuggingOccasionalHow can a coroutine produce a dangling reference, and when does it bite?
How can a coroutine produce a dangling reference, and when does it bite?
A coroutine parameter taken by reference is not copied into the frame — only the reference is. If the referent dies before the coroutine resumes, using it is UB. It bites after the first suspension, once the caller's stack has unwound.
Open full question →Common mistakes
- ✗Assuming all coroutine parameters are deep-copied into the frame, references included
- ✗Passing a temporary to a by-reference coroutine parameter and using it after a suspend
- ✗Capturing a reference to a caller local and resuming the coroutine after the caller returns
Follow-up questions
- →Why does passing the parameter by value make the snippet safe?
- →How does this hazard interact with default arguments that are temporaries?
SeniorTheoryOccasionalHow does the compiler represent a suspended coroutine's resume point?
How does the compiler represent a suspended coroutine's resume point?
The compiler rewrites the body into a state machine in the heap frame. A resume-index integer records which suspend point to continue from; resume() dispatches on it — a switch — to jump back with locals preserved.
Common mistakes
- ✗Believing a coroutine frame is a saved CPU/stack snapshot rather than a generated state machine
- ✗Thinking resumption involves an OS-level context switch — it is just an indexed jump
- ✗Assuming frame size is dynamic; it is fixed and computed at compile time
Follow-up questions
- →Why can the compiler sometimes elide the frame's heap allocation entirely?
- →Which locals are stored in the frame versus kept in registers across a suspend?
SeniorTheoryRareWhat is symmetric transfer and which problem does it solve?
What is symmetric transfer and which problem does it solve?
Symmetric transfer is returning a coroutine_handle<> from await_suspend so the compiler resumes that coroutine via a tail call instead of a nested resume(). It solves stack overflow in long coroutine chains.
Common mistakes
- ✗Confusing symmetric transfer with multithreading — it is entirely single-threaded
- ✗Thinking it is an optimization for speed, when its real purpose is bounding stack depth
- ✗Calling
handle.resume()insideawait_suspendinstead of returning the handle, defeating the point
Follow-up questions
- →What is
std::noop_coroutineand when do you return it fromawait_suspend? - →Why does a direct nested
resume()chain grow the stack but a tail-returned handle does not?