← SEARCHABLE ARCHIVE
C / C++ · 2026-08-15

C++ from Basics to Mastery — Part 21

Understand coroutine suspension, resumption, lifetime, and cancellation

INTERACTIVE READING MODE →

The articles on this blog are written for teaching purposes. The information may contain mistakes, so verify it against your target compiler and standard library.

A coroutine can suspend a function and later resume from the same state. It does not automatically create a thread or provide asynchronous I/O; it is a language-level protocol for building asynchronous control flow.

State lives in a coroutine frame

When a function uses co_await, co_yield, or co_return, the compiler transforms it into a state machine that preserves local variables and the resume location.

Task<Texture> LoadTextureAsync(Path path)
{
    FileBytes bytes = co_await ReadFileAsync(path);
    DecodedImage image = co_await DecodeAsync(std::move(bytes));
    co_return UploadTexture(std::move(image));
}

Task and the awaitables in this example are contracts supplied by a project or asynchronous library. Standard C++20 defines coroutine transformation, but it does not provide a universal executor or networking event loop.

co_await asks an awaitable how to suspend

Conceptually, an awaitable answers three questions.

await_ready   Is the result available without suspension?
await_suspend Who owns the suspended handle and resumes it?
await_resume  What value or error is produced after resumption?

If work is already complete, execution may continue immediately. Otherwise the coroutine handle must be transferred safely to a scheduler or I/O operation and resumed exactly once.

Frame ownership is the central lifetime problem

Define what happens when the caller immediately drops the returned task: does the operation cancel, detach, or remain owned elsewhere? A callback that resumes a destroyed frame causes use-after-free.

References captured by the coroutine may also expire while it is suspended.

Task<void> ShowLater(const Player& player)
{
    co_await NextFrame();
    Draw(player.Name); // Is player still alive?
}

Copy required data into the frame, use a stable handle, or make the caller's lifetime guarantee explicit.

Cancellation and errors are protocols too. Cancellation should wake suspended operations, allow cleanup to run, and propagate a result to the awaiting task. The promise type decides whether an exception is stored and rethrown at co_await or represented as an error value.

Coroutines read like sequential code while executing across multiple moments. Document frame ownership, executor, resumption thread, cancellation, and error propagation, then test the exact Task implementation your project uses.