Contract Programming vs Explicit Checks
Design-by-contract preconditions, postconditions, and invariants versus hand-rolled if-guards scattered through your functions. One declares the rules; the other reimplements them, badly, in every caller.
The short answer
Contract Programming over Explicit Checks for most cases. Contracts state the rule once, where it belongs, as part of the signature — so the obligation is documented, enforced, and impossible to silently skip.
- Pick Contract Programming if own a real codebase with shared invariants, want the spec to live with the code, and have language or library support (Eiffel, Clojure spec, Python icontract, assertions) to enforce it
- Pick Explicit Checks if at a trust boundary handling untrusted input — network, user forms, file parsing — where a violation is a normal runtime event, not a programmer bug, and must return a clean error
- Also consider: They are not enemies. Use explicit checks at the perimeter to validate the outside world; use contracts in the interior to enforce what your own code already promised. The mistake is using explicit checks for everything and calling the duplication 'defensive programming.'
— Nice Pick, opinionated tool recommendations
What they actually are
Contract programming attaches obligations to code: a precondition the caller must satisfy, a postcondition the function guarantees, and invariants the type maintains. The signature becomes the spec, and a violation points the finger precisely — caller broke the precondition, implementation broke the postcondition. Explicit checks are the manual version: if-statements you sprinkle at function tops, return-early guards, hand-written validation that re-asserts the same expectations at every call site. Both aim at the same target, correctness under bad inputs, but they put the knowledge in opposite places. Contracts centralize the rule on the callee; explicit checks smear it across every caller who remembered to write it. That single difference, where the truth lives, decides almost everything that follows: who maintains it, who can skip it, and what happens the day the rule changes and somebody updates four of the seven copies.
Where explicit checks rot
The fatal flaw of explicit checks isn't that they're wrong — it's that they're optional and duplicated. The same if (user == null) throw gets copy-pasted into a dozen functions, and the thirteenth author forgets, because nothing forced them. When the rule changes from 'non-null' to 'non-null and verified,' you now hunt every call site by grep and hope. Worse, explicit checks blur two different failures: untrusted input that SHOULD be rejected, and programmer mistakes that should never happen. Treating a broken internal precondition as a recoverable error code means you swallow the bug and limp forward corrupted. Reviewers can't tell intentional validation from paranoid noise, so the noise stays forever. Explicit checks are honest manual labor, and like all manual labor applied to a repeated task, they degrade exactly when you stop paying attention — which is always eventually.
Where contracts earn their keep
Contracts win because the obligation is declared once, attached to the thing it constrains, and enforced by tooling rather than discipline. You read a function signature and you know what it demands and what it promises — the contract is documentation that can't drift out of date, because it executes. In Eiffel it's native; in Python icontract/deal or plain assertions get you most of the way; Clojure's spec, Rust's debug_assert!, and Ada's contracts cover the rest. Crucially, contracts assign blame: a precondition failure indicts the caller, a postcondition failure indicts the implementation. That alone collapses debugging time. They also strip cleanly in production builds when you've earned the confidence, so the cost is a development-time check, not a runtime tax. The catch: contracts are for YOUR code's internal promises. They are not input validation, and pretending they are is the one way to misuse them.
The line between them
Draw it at the trust boundary. Outside that line — HTTP requests, form fields, parsed files, anything a human or hostile network produced — a bad value is a normal, expected event. Handle it with explicit checks that return a real error, because rejecting garbage is part of the function's job, not a bug. Inside that line, once data has been validated and admitted, every function should be allowed to ASSUME it's clean and assert that assumption with contracts. Re-validating already-validated data at every interior call is the duplication that bloats codebases and trains everyone to ignore the checks. The teams that get this wrong pick one tool for everything: all explicit checks (duplication, drift, swallowed bugs) or contracts at the perimeter (crashing on input you should have gracefully refused). Pick by which side of the trust boundary you're standing on. That's the whole discipline.
Quick Comparison
| Factor | Contract Programming | Explicit Checks |
|---|---|---|
| Where the rule lives | Once, on the callee's signature | Duplicated across every caller |
| Enforcement | Tooling/runtime enforced, can't be silently skipped | Relies on each author remembering |
| Blame assignment on failure | Precise: caller vs implementation | Ambiguous: bug or expected bad input? |
| Handling untrusted external input | Crashes; not meant for graceful rejection | Returns clean errors at the boundary |
| Maintenance when the rule changes | Edit one declaration | Grep every call site and pray |
The Verdict
Use Contract Programming if: You own a real codebase with shared invariants, want the spec to live with the code, and have language or library support (Eiffel, Clojure spec, Python icontract, assertions) to enforce it.
Use Explicit Checks if: You're at a trust boundary handling untrusted input — network, user forms, file parsing — where a violation is a normal runtime event, not a programmer bug, and must return a clean error.
Consider: They are not enemies. Use explicit checks at the perimeter to validate the outside world; use contracts in the interior to enforce what your own code already promised. The mistake is using explicit checks for everything and calling the duplication 'defensive programming.'
Contracts state the rule once, where it belongs, as part of the signature — so the obligation is documented, enforced, and impossible to silently skip. Explicit checks scatter the same logic across every caller, rot independently, and quietly disappear the day someone's in a hurry.
Related Comparisons
Disagree? nice@nicepick.dev