std Names What It Throws, and It Is One TypeStatus: Superseded by ADR-280 — its D1-D4 are D5-D8 there
Date: September 20, 2026
Target Version: Nikaia 0.0.75
Related: ADR-023 D1 (the set), D3 (payload), D4 (the two axes
and the named residue), D6 (IoError::NotFound in its worked output), D10 (the
payload as a view), ADR-024 D1 (?, the absence of a claim),
ADR-154 D3 (a type lives in its module), ADR-157
(the channel), Part I 1.3, Part I 7.1, Part III 17.1, Part III A.1
ADR-023 D1 records throws as a set of error types, derived
over the call graph, and ADR-157 made that set decide the failure
channel: where it names one type, a catch matches on that type’s variants.
For std the set said nothing. Every entry wrote throws = ["?"] —
ADR-024 D1’s ?, the absence of a claim — because these
functions fail with the language below’s errors and no Nikaia type stood for
one. The ledger’s own header said so plainly, and called it honest, which it
was. What it cost is that the column added nothing for the one library every
program uses: a caller read ["?"] as it fails, which is exactly what the
boolean before the column said.
It also put a floor under everything above it. A program that reads a file has
"?" in its set for ever, so ADR-157’s channel cannot be named,
ADR-101’s an error that newly reaches a catch is named once
has no set to diff, and NK2401 has nothing to check. Three records waiting on
one missing name.
The measurement is what made this small. Of 120 fn entries in std’s
ledger, nine throw. Two of them already name their failure (Overtaken, on
the lock’s two set(…; after:) doors) and work. Of the seven left:
| entry | what it actually fails with |
|---|---|
fs::map, fs::read, fs::read_to_string, fs::write |
std::io::Error |
io::read, io::read_to_string |
std::io::Error |
foreign::CStr::to_string |
a boxed &str, and its own doc calls it the same failure fs::read_to_string has |
So it is one type, not a surface to design. And the specification had
already named it: ADR-023 D6’s worked output writes
IoError::NotFound, Part III 13.5’s example row writes
throws = ["ConfigError", "IoError"], and Part III A.1 calls a missing file
the example of a recoverable error.
io::IoError is what std throws, and every entry says so.An enum with payload and an impl Error, like any error type Part I 7.1
describes. The seven entries above name it; the lock’s two keep Overtaken.
No entry in std writes ["?"] any more, and a test holds that, because
the easiest way to lose this is a new entry written the old way.
io, and a program that matches on it writes it out.use std::io and io::IoError::NotFound(p). That is
ADR-154 D3’s rule applied straight: what needs no prefix is
Part I 1.3’s list, and this is not on it — a name joins that list by a record
(D4 of ADR-154), and an error type is common is the argument
that rule exists to refuse.
The cost is named and accepted. fs::read throws an io type, so a program
that takes the failure apart writes a second use. A program that only
passes it on writes nothing at all, which is D8 of
ADR-023: propagation is what a call does. This is the same
trade ADR-156 D1 decided the other way for Bytes, and the
difference is which half of the program meets the name: fs::read’s result
is written down by anyone who holds it, its failure only by someone who
takes it apart.
enum IoError {
NotFound(String),
PermissionDenied(String),
NotText(String),
Other(String),
}
ADR-023 D4 makes the variants of one error type closed — a
match is exhaustive, and adding one is a breaking change, correctly — so each
of these is a lasting commitment and each is here for a reason that can be
cited:
NotFound is the specification’s own, twice: Part III A.1’s a missing
file, and D6’s IoError::NotFound.PermissionDenied is the other environmental failure a program acts on
differently rather than reports.NotText is std’s own rather than the operating system’s: three places
in std check UTF-8 and report it, and Part III 17.1 specifies the check.Other is D4’s named residue — exhaustiveness degrades to “and
anything else” — carrying what the operating system said.Everything std::io::ErrorKind distinguishes and this does not is Other. That
is the direction to be wrong in: a variant added later breaks an exhaustive
match, and one that was never needed cannot be taken back.
An operating system’s error does not carry the path, and not found without the
thing that was not found is the round trip to the user
ADR-023 D3 names. So std attaches it at the call that knew it.
Owned and not a view, which is the one place this record knowingly falls
short of ADR-023 D10: a path that came from a buffer travels in
the error as a view. That is the tether (ADR-008), which is not
built, and a lifetime on this type would reach every std signature. One
allocation on a path that is already failing is the honest price until then.
A program that reads a file has a set that says something. throws =
["io::IoError"] where it read ["?"], and a program that also throws its own
gets both — which is the one thing a set does that a boolean cannot.
No lowering changes. ADR-157 D1 names a channel only after a
type this unit declares, and a program does not declare io::IoError, so
every such function keeps the box exactly as before. The claim moves first and
the representation follows, which is the order
ADR-008’s analysis was built in for the same reason.
Three records stop waiting on a name. ADR-157’s channel can
be extended to a type a ledger describes; ADR-101’s diff has
a set to diff; NK2401 has something to check. None of them is built here.
as_text names what it read. task::as_text finishes one half of a
read_both exactly as read_to_string would have, which is its whole reason
to exist — and since D4 that includes saying which file, so it takes the path.
std failure can be the channel. That is
ADR-157 D1’s this unit declares opening up to a library’s
types, plus the question of what site a std failure carries
(ADR-023 D6) when the throw is not in the program.NK2401 and the --locked diff (ADR-101).IoError gains variants. D3’s list is short on purpose; growing
it is a breaking change and therefore a record.read_both’s own error, which is std’s internal pair vehicle and in no
ledger.crates/nikaia-std/std.contracts name
io::IoError; the type has a [type."io::IoError"] entry; the header’s
paragraph about ["?"] is rewritten to say what is true now.crates/nikaia-std/src/io.rs — the enum, IoError::of
which tells the language below’s kinds apart, Display, and impl Error.
The six fs/io entries and foreign::CStr::to_string return it.crates/nikaia/tests/std_errors.rs, one of which is the
standing claim that no std entry throws without naming what.