Producing errors
This chapter is for code that builds errors: any fallible function in bunsen,
or in a crate built on it that returns BunsenResult. What each constructor
and cause type does is in the
bunsen::errors docs. This chapter is
about the decisions those calls encode.
Choose the kind by who has to change
The kind answers one question: who or what has to change for the operation
to succeed? The errors docs ask it as an
ordered list:
- If the code has to change, the kind is
Illegal, orInternalwhen the code at fault is bunsen’s own. - If the request or its settings have to change, the kind is
Policy. - Otherwise the world disagrees:
Lookup(a key did not resolve),Unavailable(not now), orInvalidResource(present, but wrong).
Unsupported, Sys and the Other bail-out cover what is left.
Locality, and why blame is not final
The hard case is the boundary between Illegal and Policy, because the
same check can be either. A config whose fields disagree is a bug when a
program built it, and a bad setting when it was read from a file. The
function that checks the config cannot know which.
The rule is locality:
- a rule documented by the function that checks it (in its
# Errorsor# Panics) isIllegalwhen broken; - a rule set elsewhere, which this function only enforces, is
Policy.
So the checking function reports Illegal, because the rule is its own.
The code that read the config from outside the program is an input
boundary, and it re-marks what comes back with as_policy. A handful of
boundaries do this: loaders, command-line parsers, deserializers. The
alternative would be to make every check guess where its input came from.
Rules also move. A policy that becomes part of an API’s documented contract
turns Illegal, and a refusal that turns out to mean “not yet” turns
Unavailable. Pick the kind that is true today. Re-marking exists because
the answer depends on the caller.
One line, then the evidence
A message is one line, written for a person: what went wrong, naming the thing it went wrong with. Evidence goes in the details: the values that differ, the config that was rejected, the shards that are missing. Details print only in the report.
This split keeps log lines readable and test expectations stable. A
{:#?} dump in a message breaks both.
Put facts in causes
A fact that a program may want to read belongs in a typed cause, not only in the text:
- A check that a value breaks is a
ConstraintError: the owner, the field, and theRule. - A name that is not in a table is a
LookupError, with the names there are. The four “no such X; there are: …” messages in the crate all come from it. - Text that did not parse is a
ParseError, and the site chooses the kind by where the text came from: outside data givesInvalidResource, a request givesPolicy, and the program’s own text givesIllegal. - A foreign error stays the cause. Do not format it into the message. An
io::Errorgoes throughsys_at, which also names the path. There is deliberately noFrom<io::Error>, because a bare?could not say which file failed.
A subsystem with failures of its own that a caller may act on defines its own cause type. The complex handlers chapter covers that.
Add context; do not re-wrap
A function that calls another and passes its error up adds a frame with
ResultContext. It never rebuilds the
error with a longer message. Rebuilding loses the kind, unless every
variant is copied by hand, and loses the cause in any case. A specialist
further up then cannot find what it is looking for.
Document it, and test it
A try_x function’s # Errors section names each kind it returns and when,
and the cause type where a caller may want it.
STYLE.md
sets the rule. Its x twin panics with the report.
Each kind a function documents gets a test that names it, with
ErrorMatcher: the kind, plus the
one phrase or cause field the test is about.