ADR-241: A Library’s Error Travels in an Envelope Too

Status: Superseded by ADR-280 — its D1-D2 are D13-D14 there, D3 is in D10 Date: September 27, 2026 Target Version: Nikaia 0.0.228 Answers: #199, whether the list survives a hop to a caller with a bare channel of its own — ADR-170 D1’s open question Supersedes: ADR-159 D2 (a library’s error travels bare) Refines: ADR-170 D1 (the envelope is no longer the joining body’s alone) Related: ADR-240 D1 (the envelope keeps its site and list in the box), ADR-160 D1 (the generated sum), ADR-082 D1 (a grammar’s entry rule)

1. Context

ADR-159 D2 sent a library’s error bare — a function that reads a file had Result<T, io::IoError> — because no throw of the program raised it and an envelope’s first job was the site. ADR-170 D1 put the envelope back where a body joins, because an overlap that combines failures has a list to carry, and left the next question open: what a caller that only propagates such a failure does with it.

Asked by running it, the answer was worse than the list is lost. pair joins two reads and has Thrown<io::IoError>; relay returns pair() and had the bare io::IoError; nothing converts the first into the second, and rustc refused the file (From<Thrown<IoError>> is not implemented for IoError) — a correct program refused in a file nobody wrote. A generated sum had the same hole: its member for a library’s error was the bare type.

#199 said the transitive version was “a derived column like locks, and worth its own measurement rather than a guess”. Before a column, the simpler rule was measured.

2. The measurement

benches/envelope: three calls deep, each ?-propagating, 5 million calls, best of seven, three runs.

row ns/call  
bare, success 2.10 – 2.69  
envelope, success 2.06 – 2.56 ×0.95 – ×0.98
bare, failure 13.17 – 14.39  
envelope, failure 13.36 – 14.88 ×0.98 – ×1.03
bare, success, twice (control) 2.11 – 2.64 ties

Result<i64, io::IoError> is 32 bytes and Result<i64, Thrown<io::IoError>> 40: the envelope is one word. Neither path pays for it in time beyond the control’s own spread.

3. Decisions

D1 — A channel that is one library error is its envelope, Thrown<E>, everywhere.

Not only where a body joins: every function of the program whose failure is one library error type has Thrown<E>, and a generated sum’s member for one is Thrown<E> as the program’s own types’ members are. So a propagating caller has the channel its callee has, and the list goes with the failure through any number of callers — with no column to derive, because there is no condition.

A ? from a std call, which hands the error bare, puts the envelope on through From<E> for Thrown<E> — with no site, which the long form says in ADR-159 D3’s own sentence. A sum gets the same From<E> for each library member. A grammar’s entry rule is the one call of this program whose body is not written as a function here, and it hands its parse error bare, as std does.

D2 — A throw of a library’s error has a site.

ADR-159 D2’s reason — no throw here raised it — is false where one did: throw io::IoError::NotFound(p) is lowered as a program’s own error is, throwing(error, site).

D3 — A catch opens the envelope, and error.full() includes the list.

A handler over such a channel takes the envelope apart at the binding, as it does for a program’s own type (ADR-157 D2): match error names the library’s variants, {error} is its message, and throw error puts it back with what joined it. error.full() there is built from the two halves the handler holds, and it now includes the failures that joined — which it had left out for a program’s own errors as well.

4. Rejected alternatives

This site is open source. Improve this page.