Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* xref:4.coroutines/4.intro.adoc[Coroutines in Capy]
** xref:4.coroutines/4a.flow-control.adoc[Flow control]
** xref:4.coroutines/4b.tasks.adoc[The task Type]
** xref:4.coroutines/4k.results.adoc[The results Type]
** xref:4.coroutines/4c.launching.adoc[Starting Coroutines]
** xref:4.coroutines/4d.executors.adoc[Executors and Execution Contexts]
** xref:4.coroutines/4e.context-awaitable.adoc[The ContextAwaitable Protocol]
Expand Down
3 changes: 3 additions & 0 deletions doc/modules/ROOT/pages/4.coroutines/4.intro.adoc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//
// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
// Copyright (c) 2026 Michael Vandeberg
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
Expand All @@ -18,6 +19,8 @@ Capy's coroutine model is built around a single principle: asynchronous code sho
address the needs of asynchronous I/O in Capy.
* xref:4.coroutines/4b.tasks.adoc[The task Type] -- Declaring, returning values from, and
awaiting cpp:task[task<T>] coroutines.
* xref:4.coroutines/4k.results.adoc[The results Type] -- How operations report expected
failures as an error code plus payload, without throwing.
* xref:4.coroutines/4c.launching.adoc[Starting Coroutines] -- Starting coroutines with
cpp:run_async[], and binding child tasks with cpp:run[].
* xref:4.coroutines/4d.executors.adoc[Executors and Execution Contexts] -- Executors,
Expand Down
16 changes: 2 additions & 14 deletions doc/modules/ROOT/pages/4.coroutines/4b.tasks.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,8 @@ For cpp:task[task<void>], you can either use `co_return;` explicitly or let exec
[#reporting-errors]
== Reporting Errors: results

I/O operations report an expected failure as a value rather than an exception. They return the error alongside the result, in a cpp:results[results<Ts...>]: a `std::tuple` holding a `std::error_code` followed by zero or more payload values. Exceptions remain for genuine errors, such as a failed frame allocation.

A coroutine that reports errors this way returns cpp:task[task]`<results<Ts...>>`:

[source,cpp]
----
include::example$snippets/4b_tasks.cpp[tag=results]
----

Because cpp:results[] is a `std::tuple`, the whole standard tuple API applies: structured bindings, `std::tie`, `std::apply`, `std::get`, `std::tuple_cat`, comparisons, and tuple assignment. The error code is the first element, so `auto [ec, n] = co_await s.read_some(buf);` destructures it directly, and `std::tie` rebinds into existing variables without introducing new ones. Always test the error code before reading a payload; a payload's meaning when it is set is defined by the operation that produced it.

For cpp:results[results<>] -- no payload -- a `std::error_code` converts implicitly, so `co_return some_ec;` compiles. With payloads present you must supply the whole result, as `count_ready` shows.

This is the vocabulary the stream concepts and the concurrent combinators are written in. xref:4.coroutines/4g.composition.adoc[Concurrent Composition] and xref:6.streams/6.intro.adoc[Streams] both assume them.
I/O operations report an expected failure as a value rather than an exception, and a coroutine that reports errors this way returns cpp:task[task]`<results<Ts...>>`.
xref:4.coroutines/4k.results.adoc[The results Type] explains how to consume and produce a cpp:results[], which the stream concepts and the concurrent combinators all use.

== Running a Task

Expand Down
2 changes: 1 addition & 1 deletion doc/modules/ROOT/pages/4.coroutines/4g.composition.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

Headers: `<boost/capy/when_all.hpp>` and `<boost/capy/when_any.hpp>`. cpp:async_waker[] on this page is `<boost/capy/ex/async_waker.hpp>`.

Both combinators take cpp:task[task]`<results<...>>` children and return their results as a single cpp:results[]. If that type is new, read xref:4.coroutines/4b.tasks.adoc#reporting-errors[Reporting Errors] first -- everything below is expressed in them.
Both combinators take cpp:task[task]`<results<...>>` children. cpp:when_all[] returns a single cpp:results[], and cpp:when_any[] returns a `std::variant` of the error code and the children's payloads. If that type is new, read xref:4.coroutines/4k.results.adoc[The results Type] first -- everything below is expressed in them.

== Overview

Expand Down
83 changes: 83 additions & 0 deletions doc/modules/ROOT/pages/4.coroutines/4k.results.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//
// Copyright (c) 2026 Michael Vandeberg
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
// Official repository: https://github.com/cppalliance/capy
//

= The results Type
:page-mode: explanation

I/O operations in Capy report an expected failure, such as reaching the end of a stream, as a value.
Exceptions are kept for genuine errors, such as a failed frame allocation.
The value is a cpp:results[results<Ts...>], declared in:

[source,cpp]
----
include::example$snippets/4k_results.cpp[tag=include_results]
----

`results<Ts...>` is an alias for `std::tuple<std::error_code, Ts...>`.
The error code always comes first. Zero or more payload values follow it.
Stream operations return `results<std::size_t>`: an error code, then a byte count.

== Consuming results

Unpack the result with a structured binding, then test the error code before using the payload:

[source,cpp]
----
include::example$snippets/4k_results.cpp[tag=consume]
----

Compare the error code against a condition, not an exact error value.
For example, cpp:cond::eof[cond::eof] matches the end-of-stream error that Capy's streams and Corosio's sockets report.

== Payload Alongside an Error

Whether a payload is meaningful when the error code is set depends on the operation that produced it.
Many I/O operations report a meaningful partial result alongside the error.
cpp:read[] and cpp:write[] report the bytes transferred before the condition stopped them:

[source,cpp]
----
include::example$snippets/4k_results.cpp[tag=partial]
----

Given a stream that holds only `"hi"`, this read stops with cpp:cond::eof[cond::eof] and `n == 2`.
Other operations leave the payload unspecified on error, so check each operation's reference page.

== Reusing Variables with std::tie

Because `results` is a `std::tuple`, `std::tie` assigns into variables that already exist.
This suits a loop, where a structured binding would introduce new names on every pass:

[source,cpp]
----
include::example$snippets/4k_results.cpp[tag=tie]
----

The rest of the standard tuple API applies too: `std::get`, `std::apply`, `std::tuple_cat`, comparisons, and tuple assignment.

[#producing]
== Producing results

A coroutine that reports errors this way returns cpp:task[task]`<results<Ts...>>`:

[source,cpp]
----
include::example$snippets/4k_results.cpp[tag=produce]
----

For `results<>`, with no payload, a `std::error_code` converts implicitly, so `co_return some_ec;` compiles.
With payloads present you must supply the whole tuple, as `count_ready` shows.

== Where results Appears

* xref:6.streams/6.intro.adoc[Streams] -- `read_some`, `write_some`, cpp:read[] and cpp:write[] return `results<std::size_t>`.
* xref:4.coroutines/4g.composition.adoc[Concurrent Composition] -- cpp:when_all[] flattens its children's payloads into one `results`.
cpp:when_any[] returns a `std::variant` whose first alternative is the error code and whose others are the children's payloads.
* xref:8.examples/8l.async-mutex.adoc[Async Mutex] -- cpp:async_mutex[]'s `scoped_lock()` returns an error code and a lock guard.
* xref:A.specification-methods/Ac.contingencies.adoc[Contingencies] -- What a contingency is, and how `ec` signals one.
4 changes: 2 additions & 2 deletions doc/modules/ROOT/pages/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ This can be either your home-grown one or Corosio.
include::example$programs/index_page_echo.cpp[tag=full]
----
<.> The cpp:task[task<>] return type defines a coroutine that starts suspended. `any_stream` is a type-erased wrapper that works with any concrete stream implementation.
<.> Each `co_await` suspends until the I/O operation completes.
<.> Each `co_await` suspends until the I/O operation completes. The result is a cpp:results[], which the structured binding unpacks into `ec` and `n`. See xref:4.coroutines/4k.results.adoc[The results Type].
<.> You can signal failures by throwing an exception, or by returning a `std::error_code`.
<.> The condition `cond::eof` indicates reaching the end of the stream.

Expand Down Expand Up @@ -184,7 +184,7 @@ Your caller supplies the stream, and an I/O library supplies the I/O awaitables.
To keep the protocol out of headers, write it against the type-erased cpp:any_stream[] instead.
* The executor and the stop token reach your task through `co_await`.
Your functions do not take them as parameters.
* Each stream operation returns a cpp:results[] that holds an error code and a byte count.
* Each stream operation returns a cpp:results[] that holds an error code and a byte count. See xref:4.coroutines/4k.results.adoc[The results Type].
A read can return bytes and an error together.
The stream only reports what happened.
Your protocol decides whether the result is a success or a failure, for example whether a partial message is usable.
Expand Down
15 changes: 15 additions & 0 deletions doc/modules/ROOT/pages/quick-start.adoc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//
// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
// Copyright (c) 2026 Michael Vandeberg
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
Expand Down Expand Up @@ -102,9 +103,23 @@ Exceptions propagate through coroutine chains. To handle them at the top level:
include::example$snippets/quick_start.cpp[tag=errors,indent=0]
----

== Error Codes Instead of Exceptions

I/O operations do not throw for expected failures, such as reaching the end of a stream.
They report the failure as a value instead: a cpp:results[], holding an error code followed by the payload.
A task of your own can do the same, and the caller unpacks the error code first:

[source,cpp]
----
include::example$snippets/quick_start.cpp[tag=error_codes,indent=0]
----

See xref:4.coroutines/4k.results.adoc[The results Type].

== Next Steps

* xref:2.cpp20-coroutines/2.intro.adoc[Introduction To {cpp}20 Coroutines] — If you need to refresh the C++ coroutines basics
* xref:4.coroutines/4b.tasks.adoc[Tasks] — Learn how lazy tasks work
* xref:4.coroutines/4k.results.adoc[The results Type] — How operations report errors without throwing
* xref:4.coroutines/4c.launching.adoc[Starting Tasks] — Understand cpp:run_async[] in detail
* xref:4.coroutines/4d.executors.adoc[Executors and Execution Contexts] — Control where coroutines execute
1 change: 1 addition & 0 deletions test/doc/programs/index_page_echo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ capy::task<void> echo(capy::any_stream& stream) // <.>
char buf[1024];
for(;;)
{
// read_some returns results<std::size_t>: an error code, then a byte count
auto [ec, n] = co_await stream.read_some(capy::make_buffer(buf)); // <.>

auto [wec, _] = co_await capy::write(stream, capy::const_buffer(buf, n));
Expand Down
33 changes: 0 additions & 33 deletions test/doc/snippets/4b_tasks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,39 +77,6 @@ capy::task<> log_message(std::string msg)

} // namespace returning

namespace reporting_errors {

// tag::results[]
// `results<Ts...>` holds an error code `ec` plus zero or more payload
// values. A task returning one reports failure without throwing.

capy::task<capy::results<>> ensure_ready(bool ready)
{
if(! ready)
co_return std::make_error_code(std::errc::not_connected); // ec converts
co_return {}; // success
}

capy::task<capy::results<std::size_t>> count_ready(bool ready)
{
using result = capy::results<std::size_t>;
if(! ready)
co_return result{std::make_error_code(std::errc::not_connected), 0};
co_return result{std::error_code(), 42}; // success, carrying a value
}

capy::task<> use_them()
{
// `results` models the tuple protocol: ec first, then the payloads.
auto [ec, n] = co_await count_ready(true);
if(ec)
co_return; // always check ec first
(void)n; // n is only meaningful when ec is falsy
}
// end::results[]

} // namespace reporting_errors

namespace awaiting {

// tag::awaiting[]
Expand Down
Loading
Loading