ADR-357: A catch That Names Every Error Type Arriving Is Complete

Status: Accepted Date: October 15, 2026 Answers: whether a match over a catch’s error needs else where it names every type that can arrive (issue #625) Related: ADR-023 D4 (two axes: variants closed, types open; changed here); ADR-280 D18 (else with more than one type; changed here); ADR-308 D11 (NK2402, a grown set; changed here); ADR-345 D2 (a fatal error is taken only by name); ADR-351 (the set of a function value); ADR-288 D32 (the author warned of a lost promise); Part I 7.1; Part III C.3

1. Question

Is the set of error types arriving at a catch closed? The compiler infers it over the whole program (Part I 7.1), and a catch sees its sum. A match over error that names a variant of every type in it covers everything that can arrive. The rule so far asked for else all the same, because the set of types was held to be open.

2. How others do it

From memory, not checked against a current manual.

Language A handler over several error types
Rust the error is one enum; a match naming every variant is exhaustive; a library opts out with #[non_exhaustive]
Swift (typed throws) a catch over throws(E) is exhaustive over E
OCaml polymorphic variants: a match over the inferred row is exhaustive
Java checked exceptions: the list is written by hand, a catch for each is complete
Kotlin, C#, Python unchecked: a handler is never complete

3. Options

Option Why it lost, or won
A The set of types stays open: two or more need else a catch is never a proof that every failure is handled; the two axes of ADR-023 D4 keep two rules, though the reason for an open set there (a call behind dyn) no longer exists
B The set is closed: a match naming every type is complete chosen (D1-D5)

4. Decision

D1 — A match over a catch’s error that names every type in the set is complete.

The set of error types arriving at a catch is closed, as the variants of one error type are. A match over error is complete where every non-fatal type in the set is covered: each of its variants named, or the type taken by a bare name. A fatal type need not be named: else does not take it either, and it passes on (ADR-345 D2). else stays allowed, and takes whatever is not named.

D2 — A member the compiler cannot name needs else.

Where the set holds "?" (a call into code no ledger describes) or a from(p) not yet replaced by what the caller passed (ADR-351 D4), a match over it needs else: what cannot be named cannot be listed. It is NK1151.

D3 — A grown set refuses a complete match at the catch, narrated from the ledger.

When a callee’s set gains a type, a match that was complete without else no longer is, and it is NK1151. The message names the new type, the callee and the call that brings it in, from the ledger diff, as NK2401 narrates a broken borrow. A match with else, and a handler that does not match, take the new error, and are told so with NK2402, a warning, as before.

D4 — The author of a pub function is warned when its set grows.

Where the committed ledger has a set for a pub function and the build finds it larger, the build warns (NK2404) at the statement that brings the new type in, with the calls that lead there: a caller that names every type stops building. Committing the ledger acknowledges it, as ADR-288 D32 does for a lost sync. It is a warning to the author, not a classification of the change (ADR-288 rejected that: the same change breaks some callers and not others).

D5 — A complete match lowers with no catch-all arm.

The generated sum (ADR-280 D15) is matched by its members; where every one is named, no _ arm is written.

5. Reasons

Rejected: A, above. Rejected: a marker on a library’s function that its set may grow (Rust’s #[non_exhaustive]): a caller that wants robustness has else already, and a word the author must remember is the kind this language declines (ADR-258).

6. Consequences

This site is open source. Improve this page.