Coroutine Handle, Promise Type, and Awaiter
The Coroutine Handle, Promise Type, and Awaiter
Section titled “The Coroutine Handle, Promise Type, and Awaiter”This section covers the three interacting components of the C++ coroutine machinery, co_await Expression semantics, the promise type vs awaiter distinction, get_return_object() await_transform for custom suspension behavior, and yield_value/return_value for value Communication.
The Coroutine Machinery Overview
Section titled “The Coroutine Machinery Overview”The C++ coroutine mechanism consists of three interacting components [N4950 §9.5.4]:
- Coroutine handle (
std::coroutine_handle<P>): a non-owning reference to the coroutine frame [N4950 §21.4.4]. - Promise type: the communication channel between the coroutine author and the caller. Defined by the
promise_typenested type inside the return type [N4950 §9.5.4.3]. - Awaiter type: the type that controls what happens at each
co_awaitsuspension point. Discovered via the awaitable interface [N4950 §9.5.4.3].
The relationship between these three is:
\mathrm{Caller \xrightarrow{\mathrm{invoke} \mathrm{Coroutine \xleftrightarrow{\mathrm{promise\_type} \mathrm{Caller \xleftrightarrow{\mathrm{awaiter} \mathrm{co\_awaitco_await Expression Semantics [N4950 §9.5.4.3]
Section titled “co_await Expression Semantics [N4950 §9.5.4.3]”The expression co_await expr is transformed by the compiler into a sequence of calls on an awaiter object. The awaiter is obtained by the following lookup chain [N4950 §9.5.4.3]:
- If
exprhas anoperator co_awaitmember or non-member overload, the result ofexpr.operator co_await()(oroperator co_await(expr)) is the awaiter. - Otherwise, if the promise type has
await_transformThenpromise.await_transform(expr)is called, and the result is the awaiter. - Otherwise,
expritself is the awaiter.
Proof of co_await Transformation Steps
Section titled “Proof of co_await Transformation Steps”Claim: The compiler transforms co_await expr into a well-defined sequence of awaiter method Calls [N4950 §9.5.4.3].
Proof:
The compiler first resolves the awaiter through the lookup chain defined above (operator
co_await``await_transformOr identity).Let the resolved awaiter be
a. The compiler generates code equivalent to:auto&& a = <awaiter-expression>;if (!a.await_ready()) {using handle_t = std::coroutine_handle<PromiseType>;if constexpr (requires { { a.await_suspend(handle) } -> std::same_as<void>; }) {a.await_suspend(handle);// control returns to caller/resumer} else if constexpr (requires { { a.await_suspend(handle) } -> std::convertible_to<bool>; }) {if (!a.await_suspend(handle)) {// immediate resumption — goto resume_point} else {// control returns to caller/resumer}} else {// a.await_suspend returns a coroutine_handleauto h = a.await_suspend(handle);h.resume(); // symmetric transfer// control never reaches here}}// resume_point:a.await_resume();The
await_ready()check short-circuits suspension for already-completed awaitables (e.g., immediately available values).The
await_suspend()call is the suspension mechanism. It determines whether the coroutine actually suspends and what happens upon resumption.The
await_resume()call produces the value visible to theco_awaitexpression.
Once the awaiter is obtained, the compiler generates code equivalent to:
auto&& awaiter = <awaiter-expression>;if (!awaiter.await_ready()) { using handle_t = std::coroutine_handle<P>; awaiter.await_suspend(coroutine_handle); // <coroutine suspended; control returns to caller/resumer>}// <resumption point>awaiter.await_resume();The three methods that an awaiter must provide [N4950 §9.5.4.3]:
| Method | Return type | Purpose |
|---|---|---|
await_ready() | bool | If trueSkip suspension and proceed directly |
await_suspend(handle) | void``boolOr coroutine_handle<Z> | Called when coroutine suspends; controls resumption |
await_resume() | any type | Produces the result of co_await expr; called on resume |
The return type of await_suspend is critical [N4950 §9.5.4.3]:
void: the coroutine is suspended; control returns to the caller.bool: iftrueThe coroutine is suspended; iffalseIt resumes immediately.coroutine_handle<Z>: the coroutine is suspended, and the returned handle is resumed (symmetric transfer).
Detailed await_suspend Return Type Semantics
Section titled “Detailed await_suspend Return Type Semantics”| Return Type | Coroutine State After | What Happens |
|---|---|---|
void | Suspended | Control returns to the caller/resumer. The coroutine is suspended until something explicitly calls resume(). |
bool (true) | Suspended | Same as void. Used when the decision to suspend is conditional. |
bool (false) | Running | The coroutine continues immediately from the resume point. No suspension occurs. |
coroutine_handle<Z> | Suspended | The returned handle is resumed immediately. Used for symmetric transfer (tail-call optimization of coroutine chains). |
Promise Type vs Awaiter Type
Section titled “Promise Type vs Awaiter Type”The promise type is the bidirectional communication channel between the coroutine and its Caller. The awaiter type is the mechanism that controls individual suspension points.
| Aspect | Promise Type | Awaiter Type |
|---|---|---|
| Lifetime | Lives for the entire duration of the coroutine frame | Temporary — lives only for the duration of co_await |
| Purpose | Manages coroutine state, return values, exceptions | Controls individual suspend/resume behavior |
| Required by | Every coroutine (via promise_type alias) | Every co_await expression |
| Key methods | get_return_object``initial_suspend``final_suspend | await_ready``await_suspend``await_resume |
promise_type::get_return_object()
Section titled “promise_type::get_return_object()”The get_return_object() method [N4950 §9.5.4.3] is called before the coroutine body begins Execution. It produces the value that is returned to the caller. , this is a wrapper type That holds a std::coroutine_handle and provides a convenient API:
#include <coroutine>#include <iostream>#include <string>
struct Result { struct promise_type { std::string value_{};
promise_type() = default; promise_type(const promise_type&) = delete; promise_type& operator=(const promise_type&) = delete; ~promise_type() = default;
std::suspend_never initial_suspend() noexcept { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void unhandled_exception() {}
Result get_return_object() { return Result{std::coroutine_handle<promise_type>::from_promise(*this)}; }
void return_value(std::string s) { value_ = std::move(s); } };
std::coroutine_handle<promise_type> handle;
std::string& value() { return handle.promise().value_; }
explicit Result(std::coroutine_handle<promise_type> h) : handle(h) {} ~Result() { if (handle) handle.destroy(); } Result(const Result&) = delete; Result& operator=(const Result&) = delete;};
Result compute_value() { std::cout << "computing...\n"; co_return "hello from coroutine";}
int main() { Result r = compute_value(); // The coroutine ran eagerly (initial_suspend returns suspend_never) // and is now at final suspend (suspend_always) std::cout << "result: " << r.value() << "\n"; // ~Result calls handle.destroy()}await_transform for Custom Suspension Behavior
Section titled “await_transform for Custom Suspension Behavior”If the promise type defines await_transformEvery co_await expr first passes through it [N4950 §9.5.4.3]. This allows the promise to intercept and transform any awaitable into a custom awaiter, Enabling library-level control over suspension semantics:
#include <coroutine>#include <iostream>
struct TransformingPromise { TransformingPromise() = default; TransformingPromise(const TransformingPromise&) = delete; TransformingPromise& operator=(const TransformingPromise&) = delete; ~TransformingPromise() = default;
std::suspend_never initial_suspend() noexcept { return {}; } std::suspend_never final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() {}
auto get_return_object() { return std::coroutine_handle<TransformingPromise>::from_promise(*this); }
// Transform any integer into a logging awaiter auto await_transform(int value) { struct IntAwaiter { int value; bool await_ready() const noexcept { std::cout << "await_ready(" << value << ")\n"; return value == 0; } void await_suspend(std::coroutine_handle<>) const noexcept { std::cout << "await_suspend(" << value << ")\n"; } int await_resume() const noexcept { std::cout << "await_resume(" << value << ")\n"; return value * 2; } }; return IntAwaiter{value}; }};
struct Transforming { using promise_type = TransformingPromise; std::coroutine_handle<TransformingPromise> handle;};
Transforming transformed() { int a = co_await 5; int b = co_await 0; int c = co_await 3; std::cout << "a=" << a << " b=" << b << " c=" << c << "\n";}
int main() { auto coro = transformed(); coro.handle.destroy();}Output:
await_ready(5)await_suspend(5)await_resume(5)await_ready(0)await_resume(0)await_ready(3)await_suspend(3)await_resume(3)a=10 b=0 c=6await_transform and Generic Fallback
Section titled “await_transform and Generic Fallback”When the promise defines await_transformevery co_await expression must match one of its Overloads. If no overload matches, the code is ill-formed. This means you cannot co_await std::suspend_always or any other type not handled by await_transform unless you provide a Generic fallback:
template<typename T>auto await_transform(T&& awaitable) { return std::forward<T>(awaitable);}This generic fallback forwards the awaitable unchanged, allowing the default awaiter resolution to Proceed for types not explicitly handled.
yield_value and return_value
Section titled “yield_value and return_value”The promise type communicates values back to the caller through two distinct channels [N4950 §9.5.4.3]:
co_yield expris syntactic sugar forco_await promise.yield_value(expr). It suspends the coroutine and makesexpravailable to the caller.co_return exprcallspromise.return_value(expr)(orpromise.return_void()if no value) and transitions the coroutine to the final suspend point.
A promise type must define either return_value or return_voidBut not both. If the Coroutine uses co_return; (no value), return_void must be present.
#include <coroutine>#include <iostream>#include <string>#include <variant>
struct Channel { struct promise_type { std::variant<std::monostate, std::string, int> value_{};
promise_type() = default; promise_type(const promise_type&) = delete; promise_type& operator=(const promise_type&) = delete; ~promise_type() = default;
std::suspend_always initial_suspend() noexcept { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() {}
auto get_return_object() { return Channel{std::coroutine_handle<promise_type>::from_promise(*this)}; }
auto yield_value(std::string s) { value_ = std::move(s); return std::suspend_always{}; }
auto yield_value(int i) { value_ = i; return std::suspend_always{}; } };
std::coroutine_handle<promise_type> handle;
explicit Channel(std::coroutine_handle<promise_type> h) : handle(h) {} ~Channel() { if (handle) handle.destroy(); } Channel(const Channel&) = delete; Channel& operator=(const Channel&) = delete;
auto& value() { return handle.promise().value_; }
bool next() { if (!handle.done()) { handle.resume(); return !handle.done(); } return false; }};
Channel multi_type_channel() { co_yield std::string("hello"); co_yield 42; co_yield std::string("world"); co_yield 99;}
int main() { Channel ch = multi_type_channel(); while (ch.next()) { auto& v = ch.value(); if (std::holds_alternative<std::string>(v)) std::cout << "string: " << std::get<std::string>(v) << "\n"; else if (std::holds_alternative<int>(v)) std::cout << "int: " << std::get<int>(v) << "\n"; }}Coroutine Frame Lifetime and the final_suspend Decision
Section titled “Coroutine Frame Lifetime and the final_suspend Decision”The coroutine frame is heap-allocated (unless the compiler can prove it doesn”t escape) and its Lifetime is managed by std::coroutine_handle [N4950 §9.5.4]. When a coroutine reaches the final Suspend point, the behavior of final_suspend determines whether the frame is automatically Destroyed or must be destroyed manually:
std::suspend_always: The coroutine suspends at the final point. The caller (or resumer) must callhandle.destroy()to free the frame. This is required for any coroutine whose return type needs to observe the coroutine’s result after completion (e.g., aTaskthat carries a value).std::suspend_never: The coroutine’s frame is destroyed immediately upon reaching the final suspend point. The handle becomes invalid. This is used for fire-and-forget coroutines.
#include <coroutine>#include <iostream>#include <utility>
struct ScopedTask { struct promise_type { std::suspend_always initial_suspend() noexcept { return {}; } std::suspend_never final_suspend() noexcept { return {}; } ScopedTask get_return_object() { return ScopedTask{std::coroutine_handle<promise_type>::from_promise(*this)}; } void return_void() {} void unhandled_exception() {} };
std::coroutine_handle<promise_type> handle;
explicit ScopedTask(std::coroutine_handle<promise_type> h) : handle(h) {} ~ScopedTask() { if (handle) handle.destroy(); } ScopedTask(const ScopedTask&) = delete; ScopedTask& operator=(const ScopedTask&) = delete;};
ScopedTask fire_and_forget() { std::cout << "running...\n"; co_await std::suspend_always{}; std::cout << "resumed\n";}
int main() { ScopedTask t = fire_and_forget(); t.handle.resume(); // After resume: coroutine reaches final_suspend which is suspend_never, // so the frame is destroyed automatically. // handle is now invalid — do NOT call handle.destroy() again. // ~ScopedTask checks handle, but the handle is already done.}:::caution If final_suspend returns std::suspend_neverThe coroutine frame is destroyed at the Final suspend point. Calling handle.destroy() afterward on a dangling handle is undefined Behavior. If final_suspend returns std::suspend_alwaysYou must eventually call handle.destroy() or the frame leaks.
Symmetric Transfer and await_suspend Returning a Handle
Section titled “Symmetric Transfer and await_suspend Returning a Handle”When await_suspend returns a coroutine_handle<Z>The calling coroutine is suspended and the Returned handle is resumed immediately — without unwinding the stack back to the caller. This is symmetric transfer [N4950 §9.5.4.3]. It is the foundational mechanism for building coroutine Chains without stack overflow:
#include <coroutine>#include <iostream>
struct SymmetricTask { struct promise_type { std::suspend_always initial_suspend() noexcept { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() {}
SymmetricTask get_return_object() { return SymmetricTask{std::coroutine_handle<promise_type>::from_promise(*this)}; }
struct FinalAwaiter { bool await_ready() const noexcept { return false; }
std::coroutine_handle<> await_suspend( std::coroutine_handle<promise_type> h) noexcept { return h.promise().continuation_; }
void await_resume() const noexcept {} };
std::coroutine_handle<> continuation_; };
std::coroutine_handle<promise_type> handle; std::coroutine_handle<> continuation_{};
explicit SymmetricTask(std::coroutine_handle<promise_type> h) : handle(h) {}
struct Awaiter { SymmetricTask& task; bool await_ready() { return false; }
std::coroutine_handle<> await_suspend(std::coroutine_handle<> caller) { task.continuation_ = caller; return task.handle; }
void await_resume() {} };};
SymmetricTask inner_a() { std::cout << "inner_a start\n"; co_await std::suspend_always{}; std::cout << "inner_a end\n";}
SymmetricTask outer_b() { std::cout << "outer_b start\n"; auto a = inner_a(); SymmetricTask::Awaiter aw{a}; co_await aw; std::cout << "outer_b end\n";}
int main() { auto task = outer_b(); task.handle.resume(); task.handle.resume(); task.handle.destroy();}Symmetric transfer avoids stack growth when coroutines await each other in a chain. Without it, each co_await of a nested coroutine would grow the stack by one frame, leading to stack overflow for Deep chains. With symmetric transfer, the resumption is a tail call at the ABI level.
Exception Handling in Coroutines
Section titled “Exception Handling in Coroutines”When an exception escapes the coroutine body (i.e., it is not caught by a try/catch within the Coroutine), the promise’s unhandled_exception() method is called [N4950 §9.5.4.3]. The standard Library provides no default implementation — the promise type must define this method.
Exception Storage Pattern
Section titled “Exception Storage Pattern”The idiomatic pattern is to capture the exception in unhandled_exception() and rethrow it in await_resume():
#include <coroutine>#include <iostream>#include <exception>#include <stdexcept>
struct SafeTask { struct promise_type { std::exception_ptr exception_{};
std::suspend_always initial_suspend() noexcept { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void return_void() {}
void unhandled_exception() { exception_ = std::current_exception(); }
SafeTask get_return_object() { return SafeTask{std::coroutine_handle<promise_type>::from_promise(*this)}; }
struct FinalAwaiter { bool await_ready() const noexcept { return false; } std::coroutine_handle<> await_suspend( std::coroutine_handle<promise_type> h) noexcept { return h.promise().continuation_; } void await_resume() const noexcept {} };
std::coroutine_handle<> continuation_{}; };
std::coroutine_handle<promise_type> handle; std::coroutine_handle<> continuation_{};
explicit SafeTask(std::coroutine_handle<promise_type> h) : handle(h) {}
struct Awaiter { std::coroutine_handle<promise_type> handle_; std::coroutine_handle<>& continuation_;
bool await_ready() const noexcept { return handle_.done(); }
std::coroutine_handle<> await_suspend(std::coroutine_handle<> caller) { continuation_ = caller; handle_.promise().continuation_ = caller; return handle_; }
void await_resume() { if (handle_.promise().exception_) { std::rethrow_exception(handle_.promise().exception_); } } };
~SafeTask() { if (handle) handle.destroy(); } SafeTask(const SafeTask&) = delete; SafeTask& operator=(const SafeTask&) = delete;};
SafeTask failing_task() { std::cout << " about to throw\n"; throw std::runtime_error{"coroutine failure"}; co_return;}
SafeTask caller() { std::cout << " caller: awaiting failing task\n"; co_await failing_task(); std::cout << " caller: this line never executes\n"; co_return;}
int main() { auto task = caller(); task.handle.resume(); task.handle.resume(); task.handle.resume(); try { SafeTask::Awaiter aw{task.handle, task.continuation_}; aw.await_resume(); } catch (const std::exception& e) { std::cout << "caught: " << e.what() << "\n"; } task.handle.destroy();}Exception Propagation Through Coroutine Chains
Section titled “Exception Propagation Through Coroutine Chains”When coroutine A co_awaitS coroutine B, and B throws, the exception propagates through B’s unhandled_exception() to A’s await_resume(). This is analogous to exception propagation through Regular function calls, but the mechanism is explicit: the promise captures the exception, and the Awaiter rethrows it.
The chain works as follows:
- B’s coroutine body throws.
- B’s
unhandled_exception()capturesstd::current_exception(). - B reaches final suspend.
- B’s
FinalAwaitertransfers control back to A (symmetric transfer). - A’s
await_resume()(on B’s awaiter) callsstd::rethrow_exception()with B’s captured exception. - The exception propagates through A as if B’s
co_returnhad thrown.
Complete Awaitable Interface Implementation
Section titled “Complete Awaitable Interface Implementation”The following is a complete, production-ready awaitable type that supports all three await_suspend Return types and demonstrates best practices:
#include <coroutine>#include <iostream>#include <atomic>#include <chrono>#include <thread>
struct TimerAwaitable { int milliseconds;
bool await_ready() const noexcept { return milliseconds <= 0; }
void await_suspend(std::coroutine_handle<> handle) const { std::thread([ms = milliseconds, handle] { std::this_thread::sleep_for(std::chrono::milliseconds(ms)); handle.resume(); }).detach(); }
void await_resume() const noexcept {}};
struct TimerPromise { std::exception_ptr exception_{};
std::suspend_never initial_suspend() noexcept { return {}; } std::suspend_never final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() { exception_ = std::current_exception(); }
auto get_return_object() { return std::coroutine_handle<TimerPromise>::from_promise(*this); }};
struct TimerTask { using promise_type = TimerPromise; std::coroutine_handle<TimerPromise> handle;
TimerTask(std::coroutine_handle<TimerPromise> h) : handle(h) {} ~TimerTask() { if (handle) handle.destroy(); } TimerTask(const TimerTask&) = delete; TimerTask& operator=(const TimerTask&) = delete;
struct Awaiter { std::coroutine_handle<TimerPromise> handle_;
bool await_ready() const noexcept { return handle_.done(); }
void await_suspend(std::coroutine_handle<> caller) const { // In a real scheduler, this would schedule resumption // For simplicity, resume immediately on the calling thread caller.resume(); }
void await_resume() const { if (handle_.promise().exception_) { std::rethrow_exception(handle_.promise().exception_); } } };};
TimerTask timed_operation() { std::cout << " starting\n"; co_await TimerAwaitable{100}; std::cout << " after 100ms\n"; co_await TimerAwaitable{200}; std::cout << " after 200ms more\n";}
int main() { auto task = timed_operation(); // The coroutine runs eagerly (initial_suspend = suspend_never) // Give it time to complete std::this_thread::sleep_for(std::chrono::milliseconds(500));}Common Pitfalls
Section titled “Common Pitfalls”1. Dangling coroutine handles: The most common bug is forgetting to destroy() a coroutine Handle. If final_suspend returns std::suspend_always and the handle is never destroyed, the Frame leaks. Always pair handle creation with a destruction guarantee (RAII wrapper, scope guard, or .destroy() in the destructor of the return type).
2. Accessing the promise after final_suspend with suspend_never: If final_suspend returns std::suspend_neverThe frame (including the promise) is destroyed at the final suspend point. Any Access to handle.promise() afterward is undefined behavior.
3. Exception propagation: If an exception escapes the coroutine body, unhandled_exception() is Called on the promise. The default behavior (inheriting from std::exception_ptr storage) is to Capture the exception. If unhandled_exception does nothing, the exception is silently swallowed. Always either rethrow, store, or terminate in unhandled_exception.
4. await_transform hides the original type: When the promise defines await_transformEvery co_await expression passes through it. If you intend to co_await a type that does not match any await_transform overload, the compiler will error — the raw expression is never used as the Awaiter. Provide a generic fallback template<typename T> auto await_transform(T&& t) to Forward unsupported types unchanged.
5. await_ready returning true when the awaiter is not ready: If await_ready() returns true when the awaited operation has not actually completed, await_resume() will be called Immediately, potentially returning garbage or an uninitialized value. Always ensure await_ready() Accurately reflects completion status.
6. Returning bool vs coroutine_handle from await_suspend: A bool return of false Causes immediate resumption in the same call stack (no suspension occurs). A coroutine_handle Return causes the returned handle to be resumed via symmetric transfer. Confusing these two leads To subtle bugs where the wrong coroutine is resumed.
See Also
Section titled “See Also”- Stackless Coroutine Frames and Heap Allocation
- Generators (std::generator)
- Task Scheduling and Executors
Summary
Section titled “Summary”This topic covers the essential concepts and techniques related to coroutine handle, promise type, and awaiter, including key principles and practical applications.
Key concepts include:
- core concepts and definitions
- key principles and frameworks
- practical applications
- common techniques and methods
- evaluation and critical analysis
A thorough understanding of these concepts, combined with regular practice and review, is essential for mastery of this topic.
Worked Examples
Section titled “Worked Examples”Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
:::