# How to Use asio::spawn for Stackful Coroutines in C++

> Learn to use asio::spawn for stackful C++ coroutines. Execute async operations synchronously with basic_yield_context while keeping executor model compatibility.

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

---

**Use `asio::spawn` to launch stackful coroutines that suspend execution using `basic_yield_context` as a completion token, allowing asynchronous operations to appear synchronous while maintaining full compatibility with Asio's executor model.**

The `asio::spawn` function serves as the primary entry point for **stackful coroutines** in the Asio networking library, leveraging Boost.Coroutine (or Boost.Context) to maintain execution context across asynchronous operations. Unlike C++20 stackless coroutines that use `co_await`, `asio::spawn` utilizes `basic_yield_context` as a completion token that automatically resumes the coroutine when operations complete. This implementation, found in [`include/asio/spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/spawn.hpp), allows developers to write sequential code that runs asynchronously on any Asio executor.

## Core Architectural Components

Understanding the internal machinery of `asio::spawn` requires familiarity with three fundamental types defined in the Asio headers.

### spawned_thread_base

The **`spawned_thread_base`** class provides the low-level suspension and resumption machinery for stackful coroutines. Defined in [`include/asio/spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/spawn.hpp) at lines 35–50, this abstract base handles the coroutine’s cancellation state and ownership semantics. It serves as the foundation upon which the coroutine driver switches contexts when an asynchronous operation completes.

### basic_yield_context

The **`basic_yield_context<Executor>`** template acts as the completion token representing the currently running coroutine. Constructed with a pointer to the underlying `spawned_thread_base` and the associated executor, this type is defined around lines 62–130 in [`include/asio/spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/spawn.hpp). It exposes critical methods including `get_executor()` for accessing the bound executor, `reset_cancellation_state()` for renewing cancellation tracking, and `cancelled()` for querying termination requests.

### initiate_spawn

The **`initiate_spawn<Executor>`** template bridges the coroutine creation with Asio’s `async_initiate` mechanism. Declared in [`spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/spawn.hpp) and implemented in [`include/asio/impl/spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/impl/spawn.hpp), this initiation object constructs a concrete `spawned_thread_base` derived type and binds the user-provided function to the asynchronous execution context.

## Launching Your First Coroutine

To create a stackful coroutine, invoke `asio::spawn` with an execution context (or executor) and a callable accepting `asio::yield_context`. The coroutine suspends by passing the yield token to asynchronous operations, which resume execution upon completion.

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

void echo_server(asio::yield_context yield)
{
    try
    {
        asio::ip::tcp::acceptor acceptor(yield.get_executor(),
            asio::ip::tcp::endpoint(asio::ip::tcp::v4(), 12345));

        for (;;)
        {
            asio::ip::tcp::socket socket(yield.get_executor());
            acceptor.async_accept(socket, yield);

            char data[512];
            for (;;)
            {
                std::size_t n = socket.async_read_some(
                    asio::buffer(data), yield);
                if (n == 0) break;
                asio::async_write(socket,
                    asio::buffer(data, n), yield);
            }
        }
    }
    catch (std::exception& e)
    {
        std::cerr << "Echo server error: " << e.what() << "\n";
    }
}

int main()
{
    asio::io_context io;
    asio::spawn(io, echo_server);
    io.run();
}

```

**Key implementation details:**

- `asio::spawn(io, echo_server)` binds the coroutine to `io`’s default executor (lines 71–104 in [`spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/spawn.hpp)).
- Each `async_accept`, `async_read_some`, and `async_write` call receives `yield` as its completion token, causing suspension until the operation finishes.

## Serializing Execution with Strands

When coroutines must not execute concurrently with other completion handlers, bind them to a `strand` to enforce serialization. The coroutine inherits the strand’s executor, ensuring all asynchronous operations within the function execute sequentially relative to other strand-bound work.

```cpp
void serial_task(asio::yield_context yield)
{
    // All operations here execute on the strand without concurrent interference
}

int main()
{
    asio::io_context io;
    asio::strand<asio::io_context::executor_type> strand(io.get_executor());

    asio::spawn(strand, serial_task);
    io.run();
}

```

## Error Handling with yield[ec]

By default, `basic_yield_context` throws `asio::system_error` when asynchronous operations fail. To capture error codes instead, use the `operator[]` overload defined at lines 108–113 in [`include/asio/spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/spawn.hpp).

```cpp
void client(asio::yield_context yield)
{
    asio::ip::tcp::socket socket(yield.get_executor());
    asio::error_code ec;

    socket.async_connect(
        asio::ip::tcp::endpoint(
            asio::ip::make_address("127.0.0.1"), 12345),
        yield[ec]);

    if (ec)
    {
        std::cerr << "Connect failed: " << ec.message() << "\n";
        return;
    }
}

```

## Implementing Cooperative Cancellation

Stackful coroutines support cooperative cancellation through the `cancellation_state` object accessible via `basic_yield_context`. The coroutine can query its cancellation status using `yield.cancelled()` or reset the state with `reset_cancellation_state()`.

```cpp
void cancellable_task(asio::yield_context yield)
{
    for (int i = 0; i < 10; ++i)
    {
        if (yield.cancelled() != asio::cancellation_type::none)
            throw asio::system_error(asio::error::operation_aborted);

        asio::steady_timer timer(yield.get_executor(),
                                 std::chrono::seconds(1));
        timer.async_wait(yield);
    }
}

int main()
{
    asio::io_context io;
    asio::cancellation_signal sig;

    asio::spawn(io,
        [&](asio::yield_context y){ cancellable_task(y); },
        asio::detached);

    std::thread([&]{ 
        std::this_thread::sleep_for(std::chrono::seconds(3));
        sig.emit(asio::cancellation_type::terminal); 
    }).detach();

    io.run();
}

```

The cancellation slot is accessed indirectly through `yield.get_cancellation_slot()`, allowing external signals to propagate termination requests into the coroutine.

## Key Implementation Files

The following headers contain the definitive implementation of Asio’s stackful coroutine support:

- **[`include/asio/spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/spawn.hpp)** – Public interface exposing `asio::spawn`, `basic_yield_context`, and `spawned_thread_base`.
- **[`include/asio/impl/spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/impl/spawn.hpp)** – Internal implementation of `detail::initiate_spawn` and coroutine lifecycle management.
- **[`include/asio/cancellation_signal.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/cancellation_signal.hpp)** – Defines `cancellation_signal` for external termination requests.
- **[`include/asio/cancellation_state.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/cancellation_state.hpp)** – Per-coroutine cancellation state storage accessed via `basic_yield_context`.

## Summary

- **`asio::spawn`** creates stackful coroutines using Boost.Coroutine under the hood, distinct from C++20 `co_spawn` for stackless coroutines.
- **`basic_yield_context`** serves as the completion token that suspends execution and resumes automatically when asynchronous operations complete.
- **Strand binding** ensures thread-safe, serialized execution by passing a strand as the first argument to `spawn`.
- **Error handling** defaults to exceptions but supports error codes via `yield[ec]`.
- **Cancellation** is cooperative, requiring the coroutine to check `yield.cancelled()` or attach to cancellation slots.

## Frequently Asked Questions

### What is the difference between asio::spawn and asio::co_spawn?

**`asio::spawn`** implements stackful coroutines using Boost.Coroutine, where the entire call stack is preserved across suspension points via `basic_yield_context`. **`asio::co_spawn`** implements C++20 stackless coroutines using `co_await`, which do not preserve the call stack and rely on compiler-generated state machines. Use `spawn` when you need traditional stack preservation and `yield` semantics, and `co_spawn` when targeting C++20 coroutine syntax.

### How does basic_yield_context handle suspension?

When passed to an asynchronous operation such as `async_read`, the `basic_yield_context` object stores the current coroutine’s state in the underlying `spawned_thread_base` and yields control back to the Asio event loop. The operation’s completion handler then invokes the coroutine’s resumption mechanism, restoring the stack and continuing execution with the operation’s result. This process is managed by the `initiate_spawn` implementation in [`include/asio/impl/spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/impl/spawn.hpp).

### Can I use asio::spawn with custom executors?

Yes. The `spawn` function templates accept any type meeting the Executor requirements, including user-defined executors. The executor is stored within the `basic_yield_context` and retrieved via `yield.get_executor()`, ensuring that all subsequent asynchronous operations execute on the specified execution context.

### How do I check for cancellation in a coroutine?

Call `yield.cancelled()` to obtain the current `cancellation_type` enum value. If the result is not `asio::cancellation_type::none`, the coroutine should terminate gracefully or reset its state via `reset_cancellation_state()`. The cancellation slot itself is accessible through `yield.get_cancellation_slot()`, allowing the coroutine to register handlers for specific cancellation signals.