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:
|
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 |
|---|---|
|
End of stream reached |
|
No server at endpoint |
|
Peer reset connection |
|
Write to closed connection |
|
Operation timed out |
|
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
-
Sockets — Socket operations
-
Composed Operations — read() and write()