I have never used any coroutines, so the following are questions from a non-expert.

I’m not fully sure how this proposed feature interacts at the call site. It might be that this is useful only for coroutines in specific use cases.

I think that one of the most often used examples of coroutines are generators. Let’s assume that I have a generator for prime numbers and I pipe these into std::views::take(10). Somehow, after I have accessed the first 5 prime numbers the coroutine gets canceled. How would that play out when I try to access the 6th element?

On Aug 7, 2026, at 9:32 PM, Yexuan Xiao via Std-Proposals <std-proposals@lists.isocpp.org> wrote:


Over the past few years I’ve been thinking about how coroutines can support cancellation. Currently there are two routes to end the execution of a coroutine: returning a value or throwing an exception. If we use return values when a coroutine is cancelled (for example, by checking std::stop_token::stop_requested), we need to change the coroutine’s return type to std::expected and also make await_resume return std::expected. I don’t consider that ideal, at least this is not widely used. Using exceptions to cancel coroutines is the popular approach. As far as I know, most libraries do it this way. But this approach also has problems. Exceptions are expensive; if a child coroutine is cancelled, the exception is caught by the child’s promise::unhandled_exception and then re-thrown to the parent. This happens on every coroutine frame.

A few days ago I had an idea: coroutines should be allowed to be cancelled while they are suspended.

The API I designed is as follows:

void request_cancel() const;
Preconditions: *this refers to a suspended coroutine and the coroutine has not already been cancelled.
Effects: Places the coroutine in a cancelled state.
bool cancel_requested() const;
Preconditions: *this refers to a suspended coroutine.
Returns: true if request_cancel() has been called on the coroutine, false otherwise.

Once the coroutine is resumed and in the cancelled state, promise.unhandled_cancellation() is executed, followed by co_await promise.final_suspend().

The usage of request_cancel() is inside await_suspend:

struct stop_token_awaiter
{
    std::stop_token t;
    bool cancelled = false;

    bool await_ready() { return false; }

    void await_suspend(std::coroutine_handle handle)
    {
        if (t.stop_requested())
        {
            cancelled = true;
            handle.request_cancel();
        }
    }

    void await_resume()
    {
        if (cancelled)
        {
            std::unreachable();
        }
    }
};

auto operator co_await(std::stop_token token)
{
    return stop_token_awaiter{token};
}
Another use case is when a coroutine is submitted to a task queue or scheduler but gets rejected:

void await_suspend(std::coroutine_handle handle)
{
    if (!post_to_queue(handle))
    {
        handle.request_cancel();
    }
}
If the promise type is known, the type of handle can be changed to std::coroutine_handle<promise>, and cancellation information can be attached through the promise.

The purpose of providing cancel_requested() is to avoid the need for the promise to store a separate boolean for observers to check.

A typical usage of unhandled_cancellation is to store extra cancellation state inside the promise, for example by converting it into an exception:

struct task
{
    struct promise_type {
        std::exception_ptr e;
        // ...

        void unhandled_cancellation()
        {
            if (!std::coroutine_handle<promise>::from_promise(*this).cancel_requested())
            {
                std::unreachable();
            }
            e = std::make_exception_ptr(cancelled_exception{});
        }

        void unhandled_exception()
        {
            e = std::current_exception();
        }
    };
};
To propagate cancellation state between coroutines, an awaiter like the following can be designed:

struct propagate_cancellation_awaiter
{
    std::coroutine_handle<promise> child;

    bool await_ready() { return false; }

    void await_suspend(std::coroutine_handle<promise> parent)
    {
        if (child.cancel_requested())
        {
            parent.request_cancel();
        }
       
        ...
    }
    void await_resume() {}
};
The new API also makes it possible to avoid exceptions being thrown and caught within all coroutine frames. By using awaiters, exceptions can be translated into cancellation, and cancellation can be translated into exceptions:

struct exception_to_cancellation_awaiter
{
    std::coroutine_handle<promise> child;
    bool await_ready() { return false; }
    void await_suspend(std::coroutine_handle<promise> parent)
    {
        if (auto &&e = child.promise().get_exception())
        {
            parent.promise().set_exception(e);
            parent.request_cancel();
        }
        // ...
    }
    void await_resume() {}
};

struct cancellation_to_exception_awaiter
{
    std::coroutine_handle<promise> child;
    std::exception_ptr curr_e;
    bool await_ready() { return false; }
    void await_suspend(std::coroutine_handle<promise> parent)
    {
        if (auto &&e = child.promise().get_exception(); child.cancel_requested() && !e)
        {
            curr_e = std::make_exception_ptr(cancelled_exception{});
        }
        else if (e)
        {
            curr_e = e;
        }
        // ...
    }
    void await_resume()
    {
        if (curr_e)
        {
            std::rethrow_exception(e);
        }
    }
};
Only the coroutine that originates the exception needs to throw it; only the coroutine that handles the exception needs to catch it. Intermediate coroutines that merely propagate the exception do not throw or catch at all.
--
Std-Proposals mailing list
Std-Proposals@lists.isocpp.org
https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals