# How to Use asio::spawn for Stackful Coroutines: A Complete Guide

> Master asio spawn for stackful coroutines. This guide details how yield contexts enable suspended and resumed asynchronous operations for cleaner code flow.

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

---

**`asio::spawn` creates stackful coroutines that use `basic_yield_context` as a completion token, allowing asynchronous operations to suspend and resume execution while maintaining synchronous-looking code flow.**

The Asio library provides `asio::spawn` as the primary mechanism for writing stackful coroutines in C++. This function, implemented in [`include/asio/spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/spawn.hpp), enables developers to write asynchronous code that appears sequential while integrating seamlessly with Asio's executor model.

## Understanding asio::spawn Architecture

The implementation of `asio::spawn` rests on three core components that manage coroutine lifecycle and execution context.

### The spawned_thread_base Abstraction

At the heart of every spawned coroutine lies **`spawned_thread_base`**, defined in [`include/asio/spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/spawn.hpp) lines 35-50. This abstract base class handles suspension and resumption, maintains cancellation state, and manages ownership of the coroutine's execution thread. It provides the low-level machinery that the coroutine driver uses to switch contexts when asynchronous operations complete.

### basic_yield_context as a Completion Token

The **`basic_yield_context<Executor>`** serves as the completion token representing the currently running coroutine. Constructed around lines 62-130 in [`spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/spawn.hpp), this class holds a pointer to the underlying `spawned_thread_base` and the associated executor. When passed to an asynchronous operation, the yield token captures the suspension point; the coroutine resumes automatically when the operation completes.

### Cancellation State Management

Each spawned coroutine carries a **`cancellation_state`** that can be queried via `cancelled()` or reset through `reset_cancellation_state()`. These APIs appear in `basic_yield_context` (lines 24-60) and `spawned_thread_base` (lines 86-98), allowing coroutines to respond gracefully to termination requests.

## Launching Your First Coroutine with asio::spawn

To create a stackful coroutine, invoke `asio::spawn` with an executor or execution context and a callable that accepts a `yield_context` parameter. The primary overload for an executor resides at lines 71-104 in [`spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/spawn.hpp).

```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);          // <- suspend

            char data[512];
            for (;;)
            {
                std::size_t n = socket.async_read_some(
                    asio::buffer(data), yield);           // <- suspend
                if (n == 0) break;
                asio::async_write(socket,
                    asio::buffer(data, n), yield);        // <- suspend
            }
        }
    }
    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();
}

```

`asio::spawn(io, echo_server)` creates the coroutine and binds it to `io`'s default executor. The `yield` parameter is passed to each async operation; the call returns the operation's result once the coroutine resumes.

## Working with Executors and Strands

When thread safety is required, pass a **strand** as the first argument to `asio::spawn`. The coroutine inherits the strand's executor, ensuring that all async operations inside the coroutine execute without concurrent interference.

```cpp
void serial_task(asio::yield_context yield)
{
    // Operations here are serialized through the strand
    asio::steady_timer timer(yield.get_executor());
    timer.expires_after(std::chrono::seconds(1));
    timer.async_wait(yield);
}

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 in Stackful Coroutines

By default, `yield` throws a `system_error` when an asynchronous operation fails. To capture errors explicitly, use `yield[ec]` syntax, which accesses the `operator[]` overload defined at lines 108-113 in [`spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/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 Cancellation Support

Coroutines can check for cancellation requests using `yield.cancelled()`. The implementation in `spawned_thread_base` tracks cancellation state, which can be triggered via `cancellation_signal` objects.

```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::bind_cancellation_slot(sig.slot(), asio::detached));

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

    io.run();
}

```

The coroutine accesses its cancellation slot via `yield.get_cancellation_slot()`, allowing graceful termination when `sig.emit()` is called.

## Summary

- **`asio::spawn`** creates stackful coroutines that integrate with Asio's asynchronous model through `basic_yield_context`.
- **`spawned_thread_base`** (defined in [`include/asio/spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/spawn.hpp)) provides the underlying suspension and resumption machinery.
- **Strands** can be passed directly to `asio::spawn` to ensure serialized execution within the coroutine.
- **Error handling** uses `yield[ec]` to capture error codes instead of throwing exceptions.
- **Cancellation** is supported through `yield.cancelled()` and `reset_cancellation_state()`, with state managed in [`include/asio/cancellation_state.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/cancellation_state.hpp).

## Frequently Asked Questions

### What is the difference between stackful and stackless coroutines in Asio?

Stackful coroutines, created with `asio::spawn`, maintain their own stack and can suspend from anywhere within the function call tree. Stackless coroutines (C++20 coroutines) use `co_await` and require explicit suspension points. Stackful coroutines often yield simpler code for complex control flow but consume more memory per coroutine.

### How does basic_yield_context work internally?

`basic_yield_context` holds a pointer to a `spawned_thread_base` object and the associated executor. When passed to an asynchronous operation, it acts as a completion token—`initiate_spawn` (defined in [`include/asio/impl/spawn.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/impl/spawn.hpp)) creates the coroutine object, and the yield token captures the resume point. When the async operation completes, Asio invokes the token to resume the coroutine.

### Can I use asio::spawn with existing asynchronous functions?

Yes. Any Asio function accepting a completion token can accept `yield` or `yield[ec]`. This includes `async_read`, `async_write`, `async_accept`, and timer waits. The function signature must simply accept a `yield_context` parameter, and the coroutine will suspend until the operation completes.

### How do exceptions propagate in spawned coroutines?

If an exception escapes the coroutine function and a completion handler was provided to `asio::spawn`, the exception is caught and passed to the handler via `std::exception_ptr`. If using `asio::detached` or no handler, the exception propagates to the event loop's exception handling mechanism. Use `try-catch` blocks inside the coroutine function to handle errors locally.