Part III: Coroutine Machinery

This section explains the promise type and coroutine handle—the machinery that makes coroutines work.

The Promise Type

Every coroutine has an associated promise type that controls its behavior. The compiler finds the promise type through the coroutine’s return type:

struct MyTask
{
    struct promise_type { /* ... */ };  // <-- Compiler looks here
    // ...
};

MyTask my_coroutine()  // Return type determines promise_type
{
    co_return;
}

The promise object lives in the coroutine frame and acts as a communication channel between the coroutine and the outside world.

Required Promise Methods

A promise type must provide these methods:

struct promise_type
{
    // Create the return object (called immediately)
    ReturnType get_return_object();

    // Should we suspend at the start?
    Awaiter initial_suspend();

    // Should we suspend at the end?
    Awaiter final_suspend() noexcept;

    // Handle co_return; (void coroutines)
    void return_void();
    // OR handle co_return value; (non-void coroutines)
    void return_value(T value);

    // Handle exceptions
    void unhandled_exception();
};

get_return_object()

Called before the coroutine body runs. Returns the object that the coroutine function call produces:

MyTask get_return_object()
{
    return MyTask{
        std::coroutine_handle<promise_type>::from_promise(*this)
    };
}

This is how the caller gets a handle to interact with the coroutine.

initial_suspend()

Called immediately after the promise is constructed. Returns an awaiter that determines whether the coroutine starts running or suspends immediately:

// Eager: start running immediately
std::suspend_never initial_suspend() { return {}; }

// Lazy: suspend until someone resumes
std::suspend_always initial_suspend() { return {}; }

Lazy coroutines (returning suspend_always) are easier to compose because you control exactly when they start.

final_suspend()

Called after co_return or when the coroutine body ends. Returns an awaiter:

// Suspend at end: caller must destroy the frame
std::suspend_always final_suspend() noexcept { return {}; }

// Don't suspend: frame is destroyed automatically
std::suspend_never final_suspend() noexcept { return {}; }

If you suspend at final, the caller can access the result before the frame is destroyed. This is required for returning values from coroutines.

final_suspend() must be noexcept. Throwing here is undefined behavior.

return_void() / return_value()

Handle co_return statements:

// For coroutines that don't return a value
void return_void() {}

// For coroutines that return a value
void return_value(int value)
{
    result_ = value;
}

You provide one or the other, never both. The choice depends on whether your coroutine produces a result.

unhandled_exception()

Called when an exception escapes the coroutine body:

void unhandled_exception()
{
    exception_ = std::current_exception();  // Store for later
}

// Or terminate immediately
void unhandled_exception()
{
    std::terminate();
}

Storing the exception allows the awaiting coroutine to handle it when it tries to get the result.

Optional Promise Methods

Several optional methods customize behavior:

struct promise_type
{
    // Customize operator new for the coroutine frame
    void* operator new(std::size_t size);
    void operator delete(void* ptr);

    // Transform awaited expressions
    template<typename T>
    auto await_transform(T&& expr);

    // Handle co_yield
    auto yield_value(T value);
};

await_transform()

Intercepts every co_await expression in the coroutine. This enables:

  • Injecting context (executor, cancellation token) into awaitables

  • Prohibiting certain awaitable types

  • Logging or debugging

template<typename T>
auto await_transform(T&& awaitable)
{
    // Wrap to inject our executor
    return wrapped_awaitable{std::forward<T>(awaitable), executor_};
}

yield_value()

Handles co_yield expressions. Returns an awaiter:

std::suspend_always yield_value(int value)
{
    current_value_ = value;
    return {};  // Always suspend after yielding
}

co_yield x is equivalent to co_await promise.yield_value(x).

Coroutine Handle

The std::coroutine_handle<P> is a lightweight handle to a coroutine frame. It’s similar to a raw pointer—cheap to copy, but doesn’t manage lifetime.

Basic Operations

std::coroutine_handle<promise_type> h;

// Resume the coroutine
h.resume();
h();         // Same as resume()

// Destroy the coroutine frame
h.destroy();

// Check if at final suspension
bool done = h.done();

// Access the promise
promise_type& p = h.promise();

// Get from promise
h = std::coroutine_handle<promise_type>::from_promise(promise);

// Type-erased handle (loses promise access)
std::coroutine_handle<> erased = h;

Handle vs Pointer

Like a raw pointer:

  • Cheap to copy (typically one pointer)

  • Doesn’t own the resource

  • Can be null (default-constructed)

  • Can dangle if frame is destroyed

Unlike a raw pointer:

  • destroy() instead of delete

  • resume() instead of dereferencing

  • Type parameter gives access to promise

coroutine_handle<void>

The type-erased variant std::coroutine_handle<> (alias for coroutine_handle<void>) can refer to any coroutine:

void resume_any(std::coroutine_handle<> h)
{
    h.resume();  // Works for any coroutine
    // h.promise() is not available—we don't know the promise type
}

This is useful for schedulers that don’t need promise access.

Noop Coroutine

std::noop_coroutine() returns a handle to a coroutine that does nothing when resumed:

std::coroutine_handle<> await_suspend(std::coroutine_handle<> h)
{
    save_continuation(h);
    if (completed_synchronously_)
        return h;  // Resume immediately
    return std::noop_coroutine();  // Don't resume anything
}

This is essential for symmetric transfer patterns.

Putting It Together

Let’s build a complete task<T> type step by step.

Step 1: Basic Structure

template<typename T>
struct task
{
    struct promise_type;

    std::coroutine_handle<promise_type> handle_;

    explicit task(std::coroutine_handle<promise_type> h) : handle_(h) {}

    ~task()
    {
        if (handle_)
            handle_.destroy();
    }

    // Move-only
    task(task&& other) noexcept
        : handle_(std::exchange(other.handle_, nullptr)) {}
    task& operator=(task&&) = delete;
    task(task const&) = delete;
};

Step 2: Promise Type

template<typename T>
struct task<T>::promise_type
{
    std::optional<T> result_;
    std::exception_ptr exception_;

    task get_return_object()
    {
        return task{
            std::coroutine_handle<promise_type>::from_promise(*this)
        };
    }

    std::suspend_always initial_suspend() { return {}; }  // Lazy
    std::suspend_always final_suspend() noexcept { return {}; }

    void return_value(T value)
    {
        result_ = std::move(value);
    }

    void unhandled_exception()
    {
        exception_ = std::current_exception();
    }
};

Step 3: Awaitable Interface

template<typename T>
struct task
{
    // ... previous code ...

    bool await_ready() const { return false; }  // Always suspend

    std::coroutine_handle<> await_suspend(std::coroutine_handle<> caller)
    {
        // Store caller so we can resume them when we complete
        handle_.promise().continuation_ = caller;
        return handle_;  // Start running this task
    }

    T await_resume()
    {
        if (handle_.promise().exception_)
            std::rethrow_exception(handle_.promise().exception_);
        return std::move(*handle_.promise().result_);
    }
};

Step 4: Continuation Handling

Update promise to resume the awaiting coroutine:

template<typename T>
struct task<T>::promise_type
{
    std::coroutine_handle<> continuation_;
    // ... other members ...

    auto final_suspend() noexcept
    {
        struct final_awaiter
        {
            bool await_ready() noexcept { return false; }

            std::coroutine_handle<> await_suspend(
                std::coroutine_handle<promise_type> h) noexcept
            {
                // Resume whoever was waiting for us
                return h.promise().continuation_;
            }

            void await_resume() noexcept {}
        };
        return final_awaiter{};
    }
};

Step 5: Usage

task<int> inner()
{
    co_return 42;
}

task<int> outer()
{
    int x = co_await inner();  // Suspends, runs inner, resumes with result
    co_return x * 2;
}

The flow:

  1. outer() creates a suspended task

  2. Someone resumes outer

  3. outer hits co_await inner()

  4. outer suspends, storing itself as continuation

  5. inner runs, returns 42

  6. inner at final_suspend resumes outer

  7. outer resumes with x = 42

  8. outer returns 84

Building a Generator

Generators use co_yield to produce sequences:

template<typename T>
struct generator
{
    struct promise_type
    {
        T current_value_;
        std::exception_ptr exception_;

        generator get_return_object()
        {
            return generator{
                std::coroutine_handle<promise_type>::from_promise(*this)
            };
        }

        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}

        std::suspend_always yield_value(T value)
        {
            current_value_ = std::move(value);
            return {};
        }

        void unhandled_exception()
        {
            exception_ = std::current_exception();
        }
    };

    std::coroutine_handle<promise_type> handle_;

    // Iterator interface for range-for
    struct iterator { /* ... */ };
    iterator begin();
    iterator end();
};

Usage:

generator<int> iota(int start)
{
    while (true)
        co_yield start++;
}

for (int x : iota(0))
{
    if (x > 10) break;
    std::cout << x << "\n";
}

Summary

Component Role

Promise type

Controls coroutine behavior, stores results

get_return_object()

Creates the caller-visible return value

initial_suspend()

Eager vs lazy execution

final_suspend()

Cleanup and continuation

coroutine_handle<P>

Lightweight reference to coroutine frame

await_transform()

Intercept and transform co_await expressions

Next Steps

The basics are in place. Now learn the advanced techniques: