Search code examples
c++exceptionboostcoroutine

boost::asio::co_spawn does not propagate exception


I'm dabbling in coroutines in respect to boost::asio, and I'm confused by exception handling. Judging by the examples in the docs, it looks like any 'fail' error_code is turned into an exception - so I hopefully assumed that any exception thrown would also be propagated back to the co_spawn call. But that doesn't appear to be case:

#define BOOST_ASIO_HAS_CO_AWAIT
#define BOOST_ASIO_HAS_STD_COROUTINE

#include <iostream>

#include <boost/asio/awaitable.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/executor_work_guard.hpp>

namespace this_coro = boost::asio::this_coro;

boost::asio::awaitable<void> async_op()
{
    std::cout << "About to throw" << std::endl;
    throw std::runtime_error{"Bang!"};
}

int main()
{
    auto ctx = boost::asio::io_context{};
    auto guard = boost::asio::make_work_guard(ctx.get_executor());

    boost::asio::co_spawn(ctx, async_op, boost::asio::detached);

    ctx.run();
}

If this is ran in a debugger, you can see the exception being thrown, but then it just seems to hang. Pausing the debugger shows that the ctx.run() is waiting for new work (due to the executor_work_guard). So it looks like something inside boost::asio has silently swallowed the exception.

As an experiment, I switched the async operation to use boost::asio library calls:

boost::asio::awaitable<void> async_op()
{
    auto executor = co_await this_coro::executor;
    auto socket = boost::asio::ip::tcp::socket{executor};

    std::cout << "Starting resolve" << std::endl;
    auto resolver = boost::asio::ip::tcp::resolver{executor};
    const auto endpoints = co_await resolver.async_resolve("localhost",
                                                           "4444",
                                                           boost::asio::use_awaitable);



    std::cout << "Starting connect (num endpoints: " << endpoints.size() << ")" << std::endl;
    co_await boost::asio::async_connect(socket, endpoints, boost::asio::use_awaitable);
    std::cout << "Exited" << std::endl;
}

I don't have a server running on port 4444, so this should fail immediately - and it does but silently. Pausing the debugger shows that it's stuck in epoll waiting for something (I'm on Linux).

Swapping the async_connect CompletionToken to a boost::asio::redirect_error shows that the operation is failing:

co_await boost::asio::async_connect(socket,
                                    endpoints, 
                                    boost::asio::redirect_error(boost::asio::use_awaitable, ec));
std::cout << "Exited: " << ec.message() << std::endl;

Yields:

Starting resolve
Starting connect (num endpoints: 1)
Exited: Connection refused

So how do I propagate exceptions, and create them from error_codes, out of coroutines in boost::asio?


Solution

  • boost::asio::co_spawn creates a separate thread. This means that exceptions are not propagated. You can read more about this here:

    But co_spawn supports a completion handler with the signature void(std::exception_ptr, R). In your example you used boost::asio::detached which means the completion result is ignored. To propagate it simply write a custom handler.