Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvxzSKi1zR395wvYXUenVE
4.2 KiB
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.
Scope
General discipline for how code should behave when it encounters conditions it can't cleanly complete against — malformed input, an unreachable dependency, an assumption that turns out false. Two sections: what a function's contract should say about the input space before it's written, and what happens once something has actually gone wrong.
Excluded on purpose: language-specific exception syntax, logging infrastructure choices, testing-framework mechanics, and environment/verification parity ("tested" vs. "tested where it actually runs") — that last one is close enough to earn a future handbook of its own rather than a section here.
Input contract discipline
Degenerate input is the default case to design for, not an edge case bolted on after. Empty collections, duplicate entries, out-of-range values, malformed or wrong-typed data, oversized payloads, and concurrently-modified reads are not rare — they're what actually arrives once code leaves a demo. Design for that space first, and treat "obviously fine" input as the special case, not the other way around.
Validate, sanitize, and reject are three different responses — not one blurred behavior.
- Validate — check conformance against the contract, making no change to the input. Proceed only if it holds.
- Sanitize — transform nonconforming input into conforming input. The transformed value is what proceeds, treated exactly as if the caller had sent it that way to begin with.
- Reject — refuse to proceed, surfacing why (see Failure-propagation discipline for what "surfacing why" should actually look like).
Sanitize only what's meaning-preserving; reject anything that requires guessing intent. Sanitizing has a real cost: the caller's actual input is never seen again, only its cleaned-up substitute. That's fine when the correction is unambiguous — trimming incidental whitespace, normalizing Unicode form, case-folding under an already-documented case-insensitive contract. It's not fine when the correction requires guessing what the caller meant — clamping an out-of-range number to its nearest valid bound, silently truncating an oversized value, defaulting a missing required field. The test: would the caller be surprised to learn what actually happened to their input? If yes, reject instead of sanitizing — finding out via an error immediately is strictly better than finding out weeks later that a value was quietly substituted for the one actually sent. Silent sanitization of an out-of-range value and a guard that returns success without checking are the same failure shape: something couldn't proceed correctly, and produced a plausible-looking result instead of saying so.
Validate at trust boundaries; trust your own internals. Check input where it crosses from outside your control into it — user input, an external API response, a file read from disk, a message from another service. Code on the inside of that boundary, already known to satisfy its own contract (by type, by test, by the caller being code under the same control), doesn't need to re-validate what its caller already guaranteed — that's redundant defense that doubles maintenance cost without doubling safety. The boundary is where trust actually changes; that's the only place validation earns what it costs.
Before writing the implementation, write the contract's answer for each category. A function's contract should state, explicitly, what happens for: empty input, a duplicate where uniqueness was assumed, a value outside the expected range, a type mismatch, an oversized input, and a stale or concurrently-modified read. Leave any of those unanswered, and the implementation will invent an answer under latitude — usually the same way, by quietly doing something plausible instead of refusing. Writing the answer down first is what closes that gap; discovering it during implementation, or worse in production, is what leaves it open.
Failure-propagation discipline
(Not yet composed.)
(Handbook in progress — sections above stand as citable units; the rest is outline.)