Skip to content
C++

co_await, co_yield, co_return

The three coroutine keywords are the only surface area of C++20 coroutines that most users ever touch. Each one is a suspension or completion point that the compiler transforms into an interaction with the coroutine's promise object. Understanding what each keyword does — and crucially, what it does not do — lets you write correct coroutine code and read error messages when something goes wrong.

The rule: keywords make the coroutine

A C++ function becomes a coroutine entirely through the presence of one of these three keywords somewhere in its body. There is no annotation, no base class, no special return type syntax. The compiler detects the keyword and rewrites the function into a coroutine — allocating a frame, wrapping the body, and wiring up all the suspension points. If none of the three keywords appears, the function is a regular function regardless of its return type.

The return type of a coroutine is called the coroutine type. It is not the type of any single yielded or returned value — it is the handle object that the caller receives. Common coroutine types are std::generator<T> (C++23) for generators, and library-defined types like task<T> or lazy<T> for async work. The coroutine type is tied to a promise type that dictates how the three keywords behave in that particular coroutine — covered in detail in the next lesson.

// All three keywords can appear in the same coroutine body.
// Any one of them is sufficient to make the function a coroutine.

#include <generator>   // C++23

std::generator<int> demo() {
    co_yield 1;          // suspend and yield 1 to the caller
    co_yield 2;          // suspend and yield 2
    co_return;           // terminate the coroutine (implicit at end-of-body too)
}

// Return type is std::generator<int>, NOT int.
// The caller receives a generator object, not the yielded values directly.
for (int v : demo())    // iterating the generator drives resumption
    std::cout << v;     // prints 1 then 2

co_yield — produce a value and pause

co_yield expr is the simplest coroutine keyword to understand. It does two things atomically: hands the value of expr to the caller (via the promise's yield_value method), and suspends the coroutine. When the coroutine is resumed, execution continues at the point immediately after the co_yield expression. The yielded value itself is an intermediate result — the coroutine is still alive and can yield more values.

co_yield is the right tool for generators: sequences, ranges, or any pattern where a function needs to produce a stream of values one at a time. Each call to the coroutine's iterator produces the next value and re-suspends.

#include <generator>

// Infinite sequence of squares: 0, 1, 4, 9, 16, …
std::generator<long long> squares() {
    for (long long i = 0; ; ++i)
        co_yield i * i;    // yield, suspend, resume here next time
}

// Consume the first 5 squares
#include <ranges>
for (long long v : squares() | std::views::take(5))
    std::cout << v << ' ';   // 0 1 4 9 16
// co_yield can be used inside any control structure
std::generator<int> range(int first, int last, int step = 1) {
    for (int i = first; i < last; i += step)
        co_yield i;
    // Implicit co_return at end of body
}

for (int v : range(0, 10, 2))
    std::cout << v << ' ';   // 0 2 4 6 8

Key distinction: co_yield vs return

co_yield v passes v to the caller and suspends — the coroutine frame remains alive. Plain return is illegal in a coroutine body. The coroutine-equivalent of return is co_return, which terminates the coroutine irreversibly.

co_await — suspend until a result is ready

co_await expr is the most powerful and most complex keyword. It suspends the current coroutine until an asynchronous operation represented by expr completes, then resumes execution with the result. From the programmer's perspective it reads like a synchronous call — you write auto result = co_await someAsyncOp() and the result is available on the next line — but under the hood, the thread is not blocked; other work can run while the operation is in flight.

The expression after co_await must be an awaitable: a type that implements the awaitable protocol. The compiler transforms the co_await expression into calls to three methods on the awaitable:

await_ready() → bool

Called first. If the result is already available (e.g., cached, or a trivially-completed operation), return true to skip suspension entirely — no coroutine frame save, no scheduler involvement.

await_suspend(handle) → void | bool | handle

Called if await_ready() returned false. The coroutine is suspended before this call. The method receives a handle to the suspended coroutine and is responsible for scheduling its resumption (e.g., registering a callback that calls handle.resume() when the I/O completes). Returning void or true means the calling thread continues elsewhere. Returning false immediately resumes the coroutine. Returning another handle transfers execution to that coroutine (symmetric transfer).

await_resume() → T

Called when the coroutine is resumed. Its return value becomes the value of the entire co_await expression. This is where you extract the result of the async operation.

// Conceptual expansion of:  auto result = co_await someAwaitable;
// ↓ compiler generates roughly this:

auto&& awaitable = someAwaitable;
if (!awaitable.await_ready()) {
    // Suspend the coroutine, register a callback
    awaitable.await_suspend(current_coroutine_handle);
    // ← caller/scheduler runs here while we're suspended
    // ← something calls current_coroutine_handle.resume() later
}
// Resumed! Get the value:
auto result = awaitable.await_resume();

You rarely implement the awaitable protocol yourself. Library types (futures, timers, sockets) provide it. The two standard-library awaitables you will see constantly are utility types for controlling suspension explicitly:

#include <coroutine>

// std::suspend_always — always suspends (await_ready returns false)
// Used to suspend the coroutine unconditionally at a given point.
co_await std::suspend_always{};   // suspend, wait for someone to resume me

// std::suspend_never — never suspends (await_ready returns true)
// Used as a no-op await, or as the initial/final_suspend in custom promise types.
co_await std::suspend_never{};    // no-op, execution continues immediately

// Example: a coroutine that prints its own steps and exposes a handle
struct StepCoroutine {
    struct promise_type {
        StepCoroutine get_return_object() {
            return StepCoroutine{
                std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        std::suspend_always initial_suspend() { return {}; } // start suspended
        std::suspend_always final_suspend() noexcept { return {}; } // end suspended
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };
    std::coroutine_handle<promise_type> handle;
};

StepCoroutine steps() {
    std::cout << "step 1\n";
    co_await std::suspend_always{};  // pause here
    std::cout << "step 2\n";
    co_await std::suspend_always{};  // pause again
    std::cout << "step 3\n";
}

// Manually drive the coroutine:
auto coro = steps();
coro.handle.resume();   // prints "step 1"
coro.handle.resume();   // prints "step 2"
coro.handle.resume();   // prints "step 3", coroutine finishes
coro.handle.destroy();

co_return — terminate the coroutine

co_return is the coroutine equivalent of return — it ends the coroutine and optionally provides a final value. After co_return, the coroutine enters its final suspension (controlled by the promise's final_suspend()) and cannot be resumed. The coroutine frame is destroyed either immediately or when the last handle to it is released.

If a coroutine body reaches its closing brace without an explicit co_return, the compiler inserts an implicit co_return; (with no value). This is only valid if the promise type's return_void() is defined — otherwise it is a compile error.

co_return with a value — async task

// Pseudocode using a hypothetical task<T> coroutine type
task<int> computeAnswer() {
    auto partial = co_await someAsyncComputation();
    co_return partial * 2;   // stores the value in the promise
                              // caller retrieves it via future/task API
}

// The return value of the co_return expression must be compatible
// with the promise's return_value(T) method.

co_return void — generator or void task

std::generator<int> countdown(int from) {
    while (from > 0)
        co_yield from--;
    co_return;     // explicit termination (could also just fall off the end)
}

// Implicit co_return at end of body — same effect:
std::generator<int> countdown2(int from) {
    while (from > 0)
        co_yield from--;
    // compiler inserts co_return; here
}

Choosing the right keyword

I need to produce a sequence of values, one at a time

co_yield. Each co_yield hands a value to the caller and pauses. The coroutine resumes on the next request for a value.

e.g. Generators, lazy ranges, event streams.

I need to wait for an async operation and use its result

co_await. The coroutine suspends without blocking the thread. Execution resumes with the result when the operation completes.

e.g. Async I/O, network requests, timers, inter-coroutine calls.

I need to end the coroutine and optionally return a final value

co_return. Use it like return — with or without a value. The coroutine cannot run after co_return.

e.g. Completing a task<T>, ending a generator after a sentinel condition.

Can I mix co_yield and co_await in one coroutine?

Yes, if the promise type supports both. Standard generator types usually only support co_yield. Custom async coroutine types may support both.

e.g. An async generator that fetches pages of data could co_await a network call inside a co_yield loop.

co_yield vs co_await: the key distinction

Both keywords suspend the coroutine, so new learners often confuse them. The difference is in direction: does the coroutine want to push data out to the caller, or does it want to pull data in from an async source?

co_yield value

Direction: out. I have a value for you.

  • The coroutine produces a value
  • The caller receives it immediately
  • Used in generators / sequences
  • Caller drives resumption by asking for the next value
co_await awaitable

Direction: in. I need something from you.

  • The coroutine waits for an external result
  • The operation schedules its own completion
  • Used in async I/O / inter-coroutine calls
  • The awaitable's scheduler drives resumption

What the compiler generates

Every coroutine is transformed by the compiler into a heap-allocated state machine. The coroutine's local variables become data members of the coroutine frame. The body becomes a switch-based state machine: each suspension point corresponds to a state, and the frame stores which state to resume at. The promise object — part of the frame — provides the hooks that customise co_yield, co_await, and co_return for the particular coroutine type.

// Conceptual view of what the compiler does with a simple generator:

std::generator<int> count_to(int n) {
    for (int i = 0; i < n; ++i)
        co_yield i;
}

// The compiler transforms this into approximately:
struct __count_to_frame {
    promise_type promise;      // generator's promise
    int n;                     // parameter
    int i = 0;                 // local variable
    int __state = 0;           // which suspension point to resume at

    void resume() {
        switch (__state) {
        case 0:
            for (; i < n; ++i) {
                promise.yield_value(i);  // co_yield i
                __state = 1;
                return;                  // suspend
            case 1: ;                   // resume here
            }
        }
        // coroutine body complete — final_suspend()
    }
};
Sign in to track progress