c++boostboost-asioc++-coroutineboost-coroutine

Exceptions disappear due to boost::asio::co_spawn how to propagate via io_context::run


Before coroutines, when an exception was thrown out of a callback, like via boost::asio::post the exception would propagate out of boost::asio::io_context::run(). However if one uses coroutines via boost::asio::co_spawn, in a fire and forget mode like with boost::asio::detached the exceptions are not thrown out of run(). In similar question an anwser, https://stackoverflow.com/a/68042350/3537677, refered to using a completion handler with the signature of void (std::exception_ptr,...) however I could not get the code executed in that handler at all.

So how do I get exceptions out of co_routines propagated to io_context::run or, if that is not possible, to define a exception handling clause i.e. to co_spawn? My MRE:

#include <iostream>
#include <boost/asio.hpp>

boost::asio::awaitable<void>  coroutine() {
    std::cout << "Coroutine executes...\n";
    throw std::runtime_error("Test throw from coroutine!");
    co_return;
}

int main(int argc, char* argv[]) {
    boost::asio::io_context context;

    boost::asio::co_spawn(context, coroutine(), boost::asio::detached);
    boost::asio::co_spawn(context, coroutine(), [] (std::exception_ptr ptr) {
        std::cout << "Rethrowing in completion handler\n"; //Doesn't get executed
        throw ptr;
    });

    boost::asio::post(context, [] () {
        throw std::runtime_error("Test throw from post!");
    });

    std::thread t([&context]() {
        try {
            while (true) {
                context.run();
                return;
            }
        } catch (std::exception &e) {
            std::cerr << "Exception in context::run(): " << e.what() << "\n";
        }
    });
    t.join();
}

Actual Output:

Coroutine executes...
Coroutine executes...
Exception in context::run(): Test throw from post!

Process finished with exit code 0

Desired Output:

...
Coroutine executes...
std::cout << "Rethrowing in completion handler\n";
Exception in context::run(): Test throw from coroutine!
Exception in context::run(): Test throw from post!
Process finished with exit code 0

Solution

  • throw ptr doesn't do what you think it does.

    Use

    if (ptr) {
        std::cout << "Rethrowing in completion handler" << std::endl;
        std::rethrow_exception(ptr);
    }
    

    Next up, handling exceptions from io_context is subtly different: you stop the io_context prematurely.

    Slightly simplified and improved output:

    Live On Compiler Explorer

    #include <iostream>
    #include <boost/asio.hpp>
    
    boost::asio::awaitable<void> coroutine() {
        std::cout << "Coroutine executes..." << std::endl;
        throw std::runtime_error("Test throw from coroutine!");
        co_return;
    }
    
    int main() {
        boost::asio::io_context context;
    
        boost::asio::co_spawn(context, coroutine, boost::asio::detached);
        boost::asio::co_spawn(context, coroutine, [](std::exception_ptr ptr) {
            if (ptr) {
                std::cout << "Rethrowing in completion handler" << std::endl;
                std::rethrow_exception(ptr);
            } else {
                std::cout << "Completed without error" << std::endl;
            }
        });
    
        boost::asio::post(context, [] { throw std::runtime_error("Test throw from post!"); });
    
        while (true) {
            try {
                context.run();
                break;
            } catch (std::exception const& e) {
                std::cerr << "Exception in context::run(): " << e.what() << std::endl;
            }
        }
    }
    

    Prints

    Coroutine executes...
    Coroutine executes...
    Exception in context::run(): Test throw from post!
    Rethrowing in completion handler
    Exception in context::run(): Test throw from coroutine!