ADR-159: A Library’s Error Type Is a Channel Too, and It Travels Bare

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

1. Context

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.

2. Decisions

D1 — A channel may be named after a type a ledger describes.

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.

D2 — A library’s error travels bare, with no envelope.

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:

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.

D3 — 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.

D4 — A variant’s type is everything but the last segment.

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.

3. Consequences

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.

4. What this does not decide

5. What is built

This site is open source. Improve this page.