Error Handling

Corosio reports I/O errors through the io_result type, which carries an error code alongside any values produced by the operation.

Code snippets assume:

#include <boost/corosio.hpp>
#include <boost/capy/error.hpp>
#include <system_error>

namespace corosio = boost::corosio;
namespace capy = boost::capy;

The io_result Type

Every I/O operation returns an io_result<…​> with two public members:

  • ec — the error code (always present)

  • values — a tuple of any additional values produced by the operation

io_result also models the tuple protocol, so it can be destructured with structured bindings. It has no value() member and no conversion to bool; check for errors by testing ec.

// Void result (connect, handshake)
io_result<>                    // Contains: ec

// Single value (read_some, write_some)
io_result<std::size_t>         // Contains: ec, n (bytes transferred)

// Typed result (resolve)
io_result<resolver_results>    // Contains: ec, results

Structured Bindings Pattern

Use structured bindings to extract results:

// Void result
auto [ec] = co_await sock.connect(endpoint);
if (ec)
    std::cerr << "Connect failed: " << ec.message() << "\n";

// Value result
auto [ec, n] = co_await sock.read_some(buffer);
if (ec)
    std::cerr << "Read failed: " << ec.message() << "\n";
else
    std::cout << "Read " << n << " bytes\n";

This pattern gives you full control over error handling.

Accessing Members Directly

You can also bind the whole result and read its members:

auto result = co_await sock.connect(endpoint);
if (!result.ec)
    std::cout << "Connected successfully\n";
else
    std::cerr << "Failed: " << result.ec.message() << "\n";

The payload lives in result.values; for single-value results, prefer structured bindings, which name the value for you.

Throwing on Error

io_result never throws on its own. To turn an error into an exception, test ec and throw explicitly:

auto [ec, n] = co_await sock.read_some(buffer);
if (ec)
    throw std::system_error(ec);
// 'n' bytes were read

This keeps error handling explicit and avoids hidden control flow.

Structured Bindings vs. Explicit Throwing

Inspect ec When:

  • Errors are expected and need handling (EOF, timeout)

  • You want to log errors without throwing

  • Performance is critical (no exception overhead)

  • You need partial success information (bytes transferred)

auto [ec, n] = co_await sock.read_some(buf);
if (ec == capy::cond::eof)
{
    std::cout << "End of stream after " << n << " bytes\n";
    // Not an exceptional condition
}

Throw When:

  • Errors are truly exceptional

  • You want concise, linear code

  • Errors should propagate to a central handler

  • You don’t need partial success information

auto throw_on_error = [](auto result) {
    if (result.ec)
        throw std::system_error(result.ec);
    return result;
};

throw_on_error(co_await sock.connect(endpoint));
throw_on_error(co_await capy::write(sock, request));
auto [ec, n] = throw_on_error(co_await capy::read(sock, buffer));

Common Error Codes

I/O Errors

Error Meaning

capy::cond::eof

End of stream reached

connection_refused

No server at endpoint

connection_reset

Peer reset connection

broken_pipe

Write to closed connection

timed_out

Operation timed out

network_unreachable

No route to host

Cancellation

Cancellation does not map deterministically to a single category or value per trigger. Depending on the path, a cancelled operation may surface as capy::error::canceled (capy’s category) or as std::errc::operation_canceled (the generic category). For example, a stop token that is already requested when the operation is awaited tends to produce std::errc::operation_canceled, while cancel(), an in-flight stop-token cancel, and a syscall reporting ECANCELED tend to produce capy::error::canceled. Do not rely on the specific category or value.

Always test cancellation portably with the capy::cond::canceled condition, which matches both:

if (ec == capy::cond::canceled)
    std::cout << "Operation was cancelled\n";

EOF Handling

End-of-stream is signaled by the capy::cond::eof condition:

auto [ec, n] = co_await capy::read(stream, buffer);
if (ec == capy::cond::eof)
{
    std::cout << "Stream ended, read " << n << " bytes total\n";
    // This is often expected, not an error
}
else if (ec)
{
    std::cerr << "Unexpected error: " << ec.message() << "\n";
}

When you throw on read errors, filter out EOF if it is expected:

auto [ec, n] = co_await capy::read(stream, response);
if (ec && ec != capy::cond::eof)
    throw std::system_error(ec);
// EOF is expected when server closes connection

Partial Success

Some operations may partially succeed before an error:

auto [ec, n] = co_await capy::write(stream, large_buffer);
if (ec)
{
    std::cerr << "Error after writing " << n << " of "
              << buffer_size(large_buffer) << " bytes\n";
    // Can potentially resume from here
}

The composed operations (read(), write()) return the total bytes transferred even when returning an error.

Error Categories

Corosio uses std::error_code, which supports categories:

if (ec.category() == std::system_category())
    // Operating system error

if (ec.category() == std::generic_category())
    // Portable POSIX-style error

Capy’s own errors (eof, canceled) don’t expose a public category accessor; match them by condition instead, as shown next.

Comparing Errors

Use error conditions for portable comparison:

// Specific error (platform-dependent)
if (ec == std::errc::connection_refused)
    // ...

// Error condition (portable)
if (ec == capy::cond::canceled)
    // Matches any cancellation error

if (ec == capy::cond::eof)
    // Matches end-of-stream

Exception Safety in Coroutines

When using exceptions in coroutines, caught exceptions don’t leak:

capy::task<void> safe_operation()
{
    try
    {
        auto [ec] = co_await sock.connect(endpoint);
        if (ec)
            throw std::system_error(ec);
    }
    catch (std::system_error const& e)
    {
        std::cerr << "Connect failed: " << e.what() << "\n";
        // Exception handled here, doesn't propagate
    }
}

Uncaught exceptions in a task are stored and rethrown when the task is awaited.

Example: Robust Connection

capy::task<void> connect_with_retry(
    corosio::io_context& ioc,
    corosio::endpoint ep,
    int max_retries)
{
    corosio::tcp_socket sock(ioc);
    corosio::timer delay(ioc);

    for (int attempt = 0; attempt < max_retries; ++attempt)
    {
        sock.open();
        auto [ec] = co_await sock.connect(ep);

        if (!ec)
            co_return;  // Success

        std::cerr << "Attempt " << (attempt + 1)
                  << " failed: " << ec.message() << "\n";

        sock.close();

        // Wait before retry (exponential backoff)
        delay.expires_after(std::chrono::seconds(1 << attempt));
        co_await delay.wait();
    }

    throw std::runtime_error("Failed to connect after retries");
}

Next Steps