ADR-157: The Failure Channel Is the Error Type, Where the Ledger Names One

Status: Superseded by ADR-280 — its D1-D4 are D9-D12 there Date: September 20, 2026 Target Version: Nikaia 0.0.73 Related: ADR-023 D1 (the set, inferred), D3 (an error type is an enum), D4 (the two axes), D6 (an error knows its site), D8 (propagation), §6 (what Stage 0 did), ADR-013 D3 (the boxed channel this replaces where it can), ADR-090 (_error), Part I 7.1, Part III C.1

1. Context

ADR-023 D1 records throws as a set of error types, derived over the call graph, and the ledger has written that set for a long time: throws = ["ConfigError"] is what a program with one error type gets. D3 makes an error type an enum with payload, and D4 makes the variants within one type closed, so a catch tells failures apart with the patterns the language already defines.

Between the two stood the channel. ADR-013 D3 lowers every throws to Result<T, Box<dyn Error>>, so the set had names and the program had a box — and D4’s whole first row is unreachable through one. That record’s own §6 said so, in the row it wrote about itself: a boxed channel gives up exhaustiveness entirely, which is the opposite of D4.

What that cost is not a missing feature. It is Part I 7.1’s own program:

let config = load() catch {
    match error {
        ConfigError::NotFound(p) => Config::default()
        ConfigError::BadSyntax { line, .. } => { … }
        else => throw error
    }
}

lowered to Rust in which error is a Box<dyn Error> and the arms are ConfigError variants. rustc said expected Box<dyn Error>, found ConfigError about a file the author never wrote, which is Part III C.1’s class of defect — and the page’s Status note said Implemented.

A second one rode with it. Box<dyn Error> is Box<dyn Error + 'static>, so an error carrying a view of the caller’s buffer — ConfigError::NotFound(path), the page’s own line — was borrowed data escapes outside of function (E0521). Part I 7.1 writes an error type that borrows and ADR-023 D10 says it should: a path that came from a buffer travels in the error as a view.

2. Decisions

D1 — Where the set names exactly one type this unit declares, the channel is that type.

Result<T, Thrown[E]> rather than Result<T, Box<dyn Error>>, where E is the one member of the ledger’s throws set, and Thrown[E] is the envelope D6 needs: the author’s value plus the site it was raised at.

Everything else keeps the box, and each for its own reason:

The error type carries a lifetime exactly when it carries a view, and the position decides how it is spelled — ADR-008 D9’s derivation, one position over: a function with nothing to borrow from can only write 'static.

D2 — A handler is handed the error, and the envelope is opened once.

Part I 7.1 gives the handler error, and what the page does with it is match on its variants and print it. So the envelope is split at the binding: the error into error, and everything the language put around it — the site, and the trace if this process captured one — into a local beside it. match error { ConfigError::… } is then the plain match the source wrote, f"{error}" is the message the author wrote, and error.full() is assembled from the two halves, which is the lowering’s business and not the page’s.

Split and not discarded, which the first shape of this got wrong: keeping only the site dropped the trace, so error.full() was silently two lines where ADR-036 wants three. A test that runs the program is what said so.

A handler that never reads the error binds _error (ADR-090) and there is nothing to open.

D3 — throw error passes the error on with the site it was raised at.

Not the site it was caught at. D6 wants where it was raised, and a handler that re-raised through the ordinary path would make itself the origin of an error it only passed through.

D4 — The set names an error type, never one of its variants.

D1’s set is of types and D4 of ADR-023 puts the variants on the other axis. A variant with named fields is written as a struct literal — ConfigError::BadSyntax { line, expected } — and the derivation recorded ConfigError::BadSyntax for it where the tuple form one line up recorded ConfigError. One error type, two entries, and a set of two is a set nothing can be named after.

3. Consequences

Part I 7.1’s example is a program that runs, and the test that says so runs it rather than reading the Rust: a lowering that looks right and does not compile is exactly the defect.

An error may carry a view of the caller’s buffer, which is what D10 already promised and E0521 made impossible. In a boxed channel it still cannot, and that is honest: a box is 'static and always was.

The ledger is unchanged. Nothing new is derived and no column is added — D1’s set was already there and already right, but for D4’s variant. What changed is what the emitter does with it, which is what ADR-023 §6’s error type row was waiting for.

std is untouched. Every std entry writes throws = ["?"], so every function that reaches std keeps the box — which is every failing function in examples/ and benches/. That is why this change is safe to make: the corpus does not go through the new path at all, and the programs that do are the specification’s.

4. What this does not decide

5. What is built

This site is open source. Improve this page.