docs: compose the failure-handling handbook
Two sections, forged the same way as git-operations.md: input contract discipline (degenerate input as the default case, validate/sanitize/reject as distinct responses, the meaning-preserving line for when sanitizing is safe) and failure-propagation discipline (never substitute a plausible artifact for a failed operation, retry/abort/surface, one signaling convention per boundary, the anti-pattern catalogue). Environment/verification parity was scoped out deliberately — different axis (confidence-establishment, not code behavior), earns its own handbook later rather than a section here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011EBCcjJvEvhpToahqdsLLA
This commit is contained in:
+62
-4
@@ -1,7 +1,6 @@
|
||||
# Failure handling
|
||||
|
||||
> Composed 2026-08-22. General practice, not tied to any one project or language. Input contract
|
||||
> discipline is composed; failure-propagation discipline is still outline.
|
||||
> Composed 2026-08-22. General practice, not tied to any one project or language.
|
||||
|
||||
## Scope
|
||||
|
||||
@@ -63,6 +62,65 @@ or worse in production, is what leaves it open.
|
||||
|
||||
## Failure-propagation discipline
|
||||
|
||||
*(Not yet composed.)*
|
||||
**Never substitute a plausible-looking artifact for a failed operation.** When something can't
|
||||
complete its contract, the correct output is a signal that it didn't — not a value that resembles
|
||||
success closely enough to pass unnoticed. A blank result where real output should be, a guard that
|
||||
reports "fine" because it couldn't read what it was checking, a stale cached value returned as if it
|
||||
were fresh: each of these is a failure wearing success's clothes, and the cost is always paid later,
|
||||
by whoever trusted the output.
|
||||
|
||||
*(Handbook in progress — sections above stand as citable units; the rest is outline.)*
|
||||
**A check that can't verify must refuse — and "couldn't verify" must be a different state than
|
||||
"verified, and it's fine."** Collapsing both into one boolean is the mechanism behind the failure
|
||||
above: a function that returns `true` for both "the input passed validation" and "validation itself
|
||||
couldn't run" cannot be trusted by anything downstream, because the caller has no way to tell which
|
||||
one happened. Whatever the check returns, its failure-to-run and its pass result need to be
|
||||
distinguishable — a third state, a different return type, an exception path, anything that doesn't
|
||||
require the caller to assume good faith on the checker's behalf.
|
||||
|
||||
**Retry, abort, and surface are three different responses to a failure, and picking the wrong one is
|
||||
a fail-loudly violation on its own.**
|
||||
|
||||
- **Retry** only for failures that are plausibly transient and where repeating the operation doesn't
|
||||
change its meaning (a network read, not a payment). Bound it — a fixed number of attempts or a
|
||||
timeout, never unconditional — and don't let a bounded retry become an unbounded one by accident (a
|
||||
retry nested inside another retry, a caller that itself retries a function that already retries
|
||||
internally).
|
||||
- **Abort** when the failure indicates a broken invariant or a state that's unsafe to continue from —
|
||||
retrying or asking would just compound whatever's already wrong.
|
||||
- **Surface** — to a caller, a human, or an error return — when the system doesn't have enough
|
||||
information to pick correctly on its own. This is usually the right default when nothing above
|
||||
clearly applies: asking is cheaper than guessing wrong.
|
||||
|
||||
**A retry that eventually succeeds is still a failure that happened, and needs to be recorded as one,
|
||||
not discarded because the second attempt worked.** The caller downstream can get the successful
|
||||
result — that's the point of retrying — but "the first attempt failed" is real information: it's how
|
||||
a dependency's slow slide toward actually breaking becomes visible before it breaks outright, instead
|
||||
of only being visible in the moment it finally does. A retry policy that only speaks up once retries
|
||||
are exhausted throws that signal away for free.
|
||||
|
||||
**Pick one failure-signaling convention per boundary, and don't mix it with another.** Exceptions, a
|
||||
`Result`-shaped return, a sentinel value, an error code — any of these work, but only if every caller
|
||||
at that boundary can rely on which one is in play. Mixing them within the same boundary is its own
|
||||
failure mode: a caller checking a sentinel value for a function that actually raises, or catching an
|
||||
exception type a function never throws, proceeds past the real failure exactly as if it hadn't
|
||||
checked at all — the check existed, and did nothing.
|
||||
|
||||
**Anti-patterns, all variations on the same shape:**
|
||||
|
||||
- A swallowed exception (`except: pass`, an empty `catch` block) — the failure existed for one
|
||||
instruction and then didn't.
|
||||
- `continue` on a caught exception inside a loop — one bad item disappears with no record it was ever
|
||||
there, and the loop reports success for everything it actually attempted.
|
||||
- A fallback default that isn't marked as one — returning `0`, `null`, an empty collection, or a
|
||||
stale cached value in place of a failed real result, with nothing distinguishing it from a genuine
|
||||
`0`, `null`, empty collection, or fresh value.
|
||||
- A comment explaining away a shortcut instead of the code being fixed — `# TODO: handle this
|
||||
properly` sitting next to the exact silent failure it describes, left in place.
|
||||
- An unbounded retry — functionally identical to abort, except it costs resources first and never
|
||||
actually informs anyone that it gave up.
|
||||
|
||||
The same discipline shows up in test coverage: a suite that only exercises the happy path hasn't
|
||||
tested the code, it's tested the narrative about the code.
|
||||
|
||||
*(Handbook composed 2026-08-22. Both sections above stand as citable units — a chunk boundary, not
|
||||
just a heading.)*
|
||||
|
||||
Reference in New Issue
Block a user