# How to Use C++20 Coroutines with Asio Awaitable: A Complete Guide

> Master C++20 coroutines with Asio awaitable. Learn to use co_await for asynchronous I/O with asio::awaitable and asio::co_spawn. A complete guide for efficient C++ networking.

- Repository: [chriskohlhoff/asio](https://github.com/chriskohlhoff/asio)
- Tags: tutorial
- Published: 2026-07-17

---

**Use `asio::awaitable<T>` as your coroutine return type, pass `asio::use_awaitable` as the completion token to asynchronous operations, and launch coroutines with `asio::co_spawn` to write asynchronous I/O code using natural C++20 `co_await` syntax.**

The Asio library (chriskohlhoff/asio) provides first-class support for C++20 coroutines through a dedicated integration layer that transforms callback-based asynchronous operations into suspendible sequential code. This architecture centers on three key components that work together to bridge Asio’s executor model with the C++20 coroutine language feature.

## The Three Core Components of Asio Coroutines

Asio’s coroutine integration is built on a triangular relationship between the return type, completion token, and spawning mechanism. Understanding how these pieces interact is essential for writing correct asynchronous code.

### asio::awaitable<T> - The Coroutine Return Type

The **`asio::awaitable<T>`** template class serves as the return type for coroutine functions. Defined in [[`include/asio/awaitable.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/awaitable.hpp)](https://github.com/chriskohlhoff/asio/blob/master/include/asio/awaitable.hpp) (lines 50-53), this lightweight wrapper stores a pointer to an internal *awaitable frame* that drives the coroutine’s state machine. When your function returns `awaitable<T>`, the compiler generates the coroutine infrastructure that Asio manages.

### asio::use_awaitable - The Completion Token

The **`asio::use_awaitable`** completion token, declared in [[`include/asio/use_awaitable.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/use_awaitable.hpp)](https://github.com/chriskohlhoff/asio/blob/master/include/asio/use_awaitable.hpp) (lines 36-42), instructs Asio’s initiating functions to suspend the current coroutine and resume it when the asynchronous operation completes. This token transforms traditional callback-based APIs into awaitable expressions.

### asio::co_spawn - The Coroutine Launcher

**`asio::co_spawn`** is the convenience function that creates a new coroutine "thread" on a given executor. Implemented in [[`include/asio/co_spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/co_spawn.hpp)](https://github.com/chriskohlhoff/asio/blob/master/include/asio/co_spawn.hpp), this function takes an `awaitable<T>` produced by a user-defined coroutine and attaches it to an execution context, optionally accepting a completion handler for results or exceptions.

## Writing Your First Asio Coroutine

A coroutine is a normal C++ function that returns `asio::awaitable<T>` (or `awaitable<void>`). Inside the function, use **`co_await`** together with Asio’s async operations, passing `asio::use_awaitable` as the completion token:

```cpp
#include <asio.hpp>
#include <cstddef>

asio::awaitable<std::size_t> echo(asio::ip::tcp::socket socket)
{
    std::size_t total = 0;
    char data[1024];

    for (;;)
    {
        // Suspend the coroutine until the read completes.
        std::size_t n = co_await socket.async_read_some(
            asio::buffer(data), asio::use_awaitable);

        // Echo the data back.
        co_await asio::async_write(socket,
            asio::buffer(data, n), asio::use_awaitable);

        total += n;
    }

    co_return total;  // Return value becomes the awaitable’s result.
}

```

The `co_await` expression suspends the coroutine at the point of the asynchronous call, yielding control back to the executor until the I/O operation completes.

## Launching Coroutines with co_spawn

To execute a coroutine, you must explicitly launch it onto an executor using **`asio::co_spawn`**. This function accepts an execution context, the awaitable returned by your coroutine, and a completion handler:

```cpp
#include <asio.hpp>
#include <iostream>
#include <memory>

int main()
{
    asio::io_context ctx;

    asio::ip::tcp::acceptor acceptor(ctx,
        asio::ip::tcp::endpoint(asio::ip::tcp::v4(), 12345));

    // Launch the coroutine.
    asio::co_spawn(ctx,
        [&]() -> asio::awaitable<void>
        {
            asio::ip::tcp::socket sock = co_await acceptor.async_accept(asio::use_awaitable);
            std::size_t bytes = co_await echo(std::move(sock));
            std::cout << "Transferred " << bytes << " bytes\n";
        },
        [](std::exception_ptr ep, std::size_t transferred)
        {
            if (ep) std::rethrow_exception(ep);
            std::cout << "Echoed " << transferred << " bytes total\n";
        });

    ctx.run();
}

```

The completion handler in `co_spawn` receives two arguments: an `std::exception_ptr` for any exceptions thrown inside the coroutine, and the value returned by `co_return`. According to the source in [`co_spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/co_spawn.hpp) (lines 50-66), this overload handles the result value explicitly. A variant that ignores the result (lines 86-102) accepts a signature of `void(std::exception_ptr)`.

## Simplifying Code with Default Completion Tokens

Writing `asio::use_awaitable` on every asynchronous call becomes repetitive. You can adapt an I/O object to use that token as its default using **`asio::use_awaitable.as_default_on()`**:

```cpp
asio::awaitable<void> periodic_timer(asio::steady_timer timer)
{
    for (int i = 0; i < 5; ++i)
    {
        co_await timer.async_wait();  // No token needed here
        std::cout << "Tick " << i << "\n";
        timer.expires_after(std::chrono::seconds(1));
    }
}

int main()
{
    asio::io_context ctx;
    asio::steady_timer tm(ctx, std::chrono::seconds(1));

    // Adapt the timer to use use_awaitable by default.
    auto timer = asio::use_awaitable.as_default_on(std::move(tm));

    asio::co_spawn(ctx, periodic_timer(std::move(timer)), asio::detached);
    ctx.run();
}

```

The adaptation logic lives in [`use_awaitable.hpp`](https://github.com/chriskohlhoff/asio/blob/main/use_awaitable.hpp) (lines 94-102), which defines `executor_with_default` and the `as_default_on` member function. Once adapted, every async operation on the object automatically uses `use_awaitable`.

## Handling Cancellation in Coroutines

Coroutines inherit a cancellation state that initially supports only terminal cancellation types. You can modify this behavior from within the coroutine using **`asio::this_coro::reset_cancellation_state`**, as referenced in [`co_spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/co_spawn.hpp) (lines 103-108). This allows you to opt into finer-grained cancellation policies such as partial cancellation or total cancellation.

## Complete Working Examples

These examples compile with any C++20-compliant compiler using the Asio headers from the chriskohlhoff/asio repository (e.g., `g++ -std=c++20 -Iinclude`).

### TCP Echo Server

```cpp
#include <asio.hpp>
#include <iostream>

asio::awaitable<std::size_t> echo(asio::ip::tcp::socket socket)
{
    std::size_t total = 0;
    char data[1024];

    for (;;)
    {
        std::size_t n = co_await socket.async_read_some(
            asio::buffer(data), asio::use_awaitable);
        co_await asio::async_write(socket,
            asio::buffer(data, n), asio::use_awaitable);
        total += n;
    }

    co_return total;
}

int main()
{
    asio::io_context ctx;
    asio::ip::tcp::acceptor acc(ctx,
        asio::ip::tcp::endpoint(asio::ip::tcp::v4(), 5555));

    asio::co_spawn(ctx,
        [&]() -> asio::awaitable<void>
        {
            asio::ip::tcp::socket sock = co_await acc.async_accept(asio::use_awaitable);
            std::size_t bytes = co_await echo(std::move(sock));
            std::cout << "Session finished, " << bytes << " bytes echoed\n";
        },
        asio::detached);

    ctx.run();
}

```

### Periodic Timer with Default Token

```cpp
#include <asio.hpp>
#include <iostream>

asio::awaitable<void> ticker(asio::steady_timer timer)
{
    for (int i = 0; i < 5; ++i)
    {
        co_await timer.async_wait();  // Uses default token
        std::cout << "Tick " << i << "\n";
        timer.expires_after(std::chrono::seconds(1));
    }
}

int main()
{
    asio::io_context ctx;
    asio::steady_timer tm(ctx, std::chrono::seconds(1));

    auto timer = asio::use_awaitable.as_default_on(std::move(tm));

    asio::co_spawn(ctx, ticker(std::move(timer)), asio::detached);
    ctx.run();
}

```

## Key Source Files and Implementation Details

Understanding the implementation helps debug complex scenarios:

- **[`include/asio/awaitable.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/awaitable.hpp)** - Defines `asio::awaitable<T>` and its coroutine frame handling (lines 50-53).
- **[`include/asio/use_awaitable.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/use_awaitable.hpp)** - Declares the `use_awaitable` completion token and the `as_default_on` adapter (lines 36-42, 94-102).
- **[`include/asio/co_spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/co_spawn.hpp)** - Implements the spawning logic with multiple completion handler signatures (lines 50-66, 86-102) and cancellation state support (lines 103-108).
- **[`include/asio/detail/awaitable_frame.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/detail/awaitable_frame.hpp)** - Contains the internal state machine struct used by `awaitable`.

## Summary

- **Return Type** - Use `asio::awaitable<T>` for coroutine functions that produce values.
- **Completion Token** - Pass `asio::use_awaitable` to async operations to enable `co_await` suspension.
- **Launch Mechanism** - Use `asio::co_spawn` to attach coroutines to executors and handle completion or errors.
- **Default Tokens** - Apply `asio::use_awaitable.as_default_on()` to I/O objects to reduce boilerplate.
- **Cancellation** - Modify cancellation behavior with `asio::this_coro::reset_cancellation_state` inside coroutines.

## Frequently Asked Questions

### What compiler flags are needed for Asio C++20 coroutines?

Compile with `-std=c++20` (or `/std:c++20` on MSVC) and include the Asio header path. The library requires compiler support for the coroutine TS (generally available in GCC 10+, Clang 14+, and MSVC 2019+).

### How do I handle errors in Asio coroutines?

Exceptions thrown inside coroutines propagate to the completion handler passed to `asio::co_spawn` as an `std::exception_ptr`. Alternatively, use `asio::as_tuple(asio::use_awaitable)` to receive error codes directly without exceptions.

### Can I use Asio coroutines with multiple threads?

Yes. Coroutines launched via `co_spawn` run on the provided executor. If that executor represents a thread pool (such as `asio::thread_pool`), the coroutine will execute on those threads. The coroutine itself remains single-threaded unless you explicitly introduce synchronization.

### What is the difference between use_awaitable and use_future?

`asio::use_awaitable` integrates with C++20 coroutines and suspends the current coroutine, while `asio::use_future` returns a `std::future` suitable for blocking waits or future continuations. `use_awaitable` provides lower latency and better integration with Asio’s executor model compared to `use_future`.