Status: Superseded by ADR-280 — its D1 is in D9, D3 in D3, D4 in D12 there; D2, already displaced by ADR-241 D1, is not carried Date: September 20, 2026 Target Version: Nikaia 0.0.76 Related: ADR-157 D1 (the channel, and the this unit declares this opens), D2 (the envelope), ADR-158 (the names this needs), ADR-023 D1 (the set), D4 (the two axes), D6 (an error knows its site), D8 (propagation), ADR-154 D3 (a type lives in its module), Part I 7.1, Part III C.1
ADR-157 D1 makes the failure channel the error type where the
ledger names one, and it names one only when the type is declared by this
unit. That guard was right when it was written: nothing else could be named,
because every std entry wrote throws = ["?"].
ADR-158 removed the reason. A program that reads a file now has
throws = ["io::IoError"] — a set of exactly one named member — and still
travelled in the opaque channel, because the member is std’s type and not the
program’s. #243 carried that as a measurement rather than a
guess: every failing function in the corpus carries a named set and still
travels in the box.
Half of it turned out to be built already. A catch written directly
over a std call has always bound what that call hands back, and since
ADR-158 that is an io::IoError — so
let text = fs::read_to_string(path) catch {
match error {
io::IoError::NotFound(p) => f"no file: {p}"
else => "other"
}
}
compiles and runs with no change to this compiler at all. What did not work was
the same failure crossing a function boundary: fn load(…) -> String throws
{ return fs::read_to_string(path) } declared Box<dyn Error>, and a handler
over load met expected Box<dyn Error>, found IoError — rustc speaking
about the generated file, which is Part III C.1’s
class.
And two path rules were reading a module for a type. ConfigError::NotFound
has two segments and the type is the first; io::IoError::NotFound has three
and the type is the first two. Both the error-set derivation and the
variant-constructor exemption took the first segment, so a throw
io::IoError::NotFound(p) recorded io — the module — and the constructor
beside it was taken for a callee nothing describes. Neither was reachable until
an error type lived in a module.
Not only one this unit declares. io::IoError is std’s and is every bit as
nameable: the library publishes it, the generated file reaches it through the
prelude, and it is the whole set of every program that reads a file.
What still keeps the opaque channel is unchanged and is about the set
rather than about whose the type is: a "?" in it, two members in it, or a
method that implements a trait.
ADR-157 D2 wraps a program’s own error in a Thrown[E], which
carries the site ADR-023 D6 says an error knows. A library’s error
gets no envelope, and the reason is not economy:
throw in this program to have a site. The failure arrives
from a call. An envelope would carry the place the program received it,
which is not what D6 asks for and would read as though the program raised it.std hands the value back as it is, so propagating it is the plain ?
the language below already writes, and a catch binds what the source names.
This is also what makes the two shapes agree from the program’s side: whether
the failure came from a std call directly or through a function of its own,
error is an io::IoError either way.A program that writes throw io::IoError::NotFound(p) itself therefore records
no site. That is the cost, and it is small: a program that wants its failures to
carry where they were raised declares its own error type, which is what Part I
7.1 teaches first.
error.full() on such a failure says there is no site.In the words the opaque channel has always used for an error that came from below Nikaia: (raised below Nikaia; no site recorded). What it carries instead is what the failure was about, which ADR-158 D4 put in the payload, and which is the more useful half for an environmental failure anyway.
In both places that read one: the error-set derivation, and the exemption that tells a variant constructor from a call. A type keyed with its module (ADR-154 D3) is still one type.
The corpus goes through the new path. This is the first of the three error
records to change what examples/ lowers to: a function that reads a file now
declares Result<T, io::IoError> where it declared the box.
A handler sees the same thing either way, which is what makes the rule
teachable: error is an io::IoError whether the catch sits over the std
call or over a function that passed it on.
Two path rules stopped reading a module for a type (D4), which nothing could
reach before ADR-158 and which would have been found by the first
program to throw a library’s error.
NK2401 and the --locked diff (ADR-101).std’s does —
but nothing in the tree has one, so nothing was measured.Emitter::named_error answers Own or Library, reading the
library’s types; error_channel writes the bare name for the second.throw in such a function is Err(value); a catch over such a call
opens no envelope, because there is none.impl Full for io::IoError and for Overtaken, in
crates/nikaia-std/src/error.rs.contracts::throws::error_type and contracts::sync::reached.crates/nikaia/tests/error_types.rs, two of which compile
and run the program.