Chapter 4 — Pragmatic Paranoia
Topics 23–27 | Pages 188–225
Core Idea
Tip 36: You Can’t Write Perfect Software
Accept this axiom. Pragmatic Programmers don’t just defend against others’ bugs — they defend against their own. The techniques in this chapter all build defenses against inevitable human error, including yours.
Topic 23 — Design by Contract (p191)
Tip 37: Design with Contracts
DBC (from Bertrand Meyer / Eiffel) formalizes the rights and responsibilities of software components through three contract elements:
| Element | Meaning | Who’s responsible |
|---|---|---|
| Preconditions | What must be true for the routine to be called | The caller — routine should never be called with violated preconditions |
| Postconditions | What the routine guarantees on completion | The routine — commits to what it delivers |
| Class invariants | What must always be true from the caller’s perspective | The class — holds before and after every method call |
If both parties fulfill the contract, the program is correct. If either violates it, that’s a bug — not something to handle gracefully.
Key insight: DBC forces you to think precisely about input domain, boundary conditions, and guarantees before writing code. This clarity alone prevents many bugs.
DBC vs. Testing:
- Tests check one scenario at a time; DBC defines correctness for all cases
- Tests run at test time; contracts are active in design, development, deployment, and production
- DBC doesn’t require setup/mocking
- DBC + TDD together are complementary, not redundant
Lazy code principle: Be strict in what you accept; promise as little as possible in return.
Semantic invariants: some requirements are philosophical laws, not policies. “Err in favor of the consumer” is a semantic invariant — immutable regardless of management changes. Make these explicit and prominent.
Topic 24 — Dead Programs Tell No Lies (p203)
Tip 38: Crash Early
Every error tells you something. Don’t ignore errors by logging and re-raising then continuing — if something impossible just happened, your program is in an unknown state. Anything it does from that point is suspect.
“A dead program normally does a lot less damage than a crippled one.”
Catch and release is for fish — don’t wrap everything in try/catch just to log and re-raise. Let exceptions propagate. This keeps code cleaner and less coupled (adding a new exception type won’t silently break callers).
The Erlang/Elixir philosophy: “let it crash” and use supervisor trees to restart failed processes. This leads to high-availability systems precisely because failure is managed, not suppressed.
Topic 25 — Assertive Programming (p207)
Tip 39: Use Assertions to Prevent the Impossible
Whenever you think “this can never happen,” add an assertion. Assertions are your contract with reality — they verify assumptions that should always hold.
Rules for assertions:
- Assert things that should never happen, not expected user errors
- Don’t use assertions for input validation — those are real error conditions
- Avoid assertions with side effects (Heisenbug risk — the assertion changes the very state it’s checking)
- Leave assertions on in production — testing doesn’t find all bugs; production is where the real nasty conditions appear
“Turning off assertions when you deliver a program to production is like crossing a high wire without a net because you once made it across in practice.”
If a specific assertion has measurable performance impact, disable just that one — but leave the rest on.
Topic 26 — How to Balance Resources (p212)
Tip 40: Finish What You Start
Tip 41: Act Locally
The routine or object that allocates a resource owns its deallocation.
Rules for resource balancing:
- Deallocate in reverse order of allocation — prevents orphaning when one resource references another
- Always allocate in the same order across different code paths — prevents deadlocks (A waits for B’s resource; B waits for A’s resource)
- Reduce scope — use block-scoped resource acquisition (Ruby
File.open do |f|, Rust{}, Pythonwith) so cleanup is guaranteed - For exceptions, allocate before the
tryblock, then usefinallyto deallocate — don’t put allocation insidetryif deallocation is infinally
Bad pattern:
begin
thing = allocate_resource() # bad: what if this throws?
process(thing)
finally
deallocate(thing) # thing may be nil
end
Good pattern:
thing = allocate_resource()
begin
process(thing)
finally
deallocate(thing)
end
Consider all resources over time: log files, database records, debug files. Anything finite needs a balancing mechanism.
Topic 27 — Don’t Outrun Your Headlights (p222)
Tip 42: Take Small Steps — Always
Tip 43: Avoid Fortune-Telling
We can only see a limited distance ahead. Software “headlights” have a throw distance too. Beyond one or two steps, estimates become speculation.
The rate of feedback is your speed limit. Feedback sources:
- REPL results → understanding of APIs and algorithms
- Unit tests → correctness of last change
- User demos → feature fit and usability
Signs you’re fortune-telling (and should pull back):
- Estimating completion dates months out
- Designing for future maintenance or extendability beyond what you can see
- Guessing user needs that haven’t surfaced yet
- Predicting tech availability
Instead: design code to be replaceable. Replaceable code naturally leads to good coupling, cohesion, and DRY.
Black swans — significant, unexpected events that upend “obvious” predictions. Around the time of the 1st edition, everyone debated “Motif vs. OpenLook for GUI dominance.” The web made the question irrelevant. You can’t predict black swans, but you can build code that survives them.
Key Takeaways
- You can’t write perfect software — build defenses against your own mistakes, not just others’.
- DBC makes contracts explicit: preconditions (caller’s duty), postconditions (routine’s guarantee), invariants (class’s promise).
- Crash early — a dead program does less damage than one that continues in an invalid state.
- Assert the impossible — leave assertions on in production; that’s where the impossible happens.
- Whoever allocates a resource owns its deallocation; use scoped lifetime features whenever available.
- Take small steps with frequent feedback; don’t try to see further than your headlights reach.