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:

ElementMeaningWho’s responsible
PreconditionsWhat must be true for the routine to be calledThe caller — routine should never be called with violated preconditions
PostconditionsWhat the routine guarantees on completionThe routine — commits to what it delivers
Class invariantsWhat must always be true from the caller’s perspectiveThe 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:

  1. Deallocate in reverse order of allocation — prevents orphaning when one resource references another
  2. Always allocate in the same order across different code paths — prevents deadlocks (A waits for B’s resource; B waits for A’s resource)
  3. Reduce scope — use block-scoped resource acquisition (Ruby File.open do |f|, Rust {}, Python with) so cleanup is guaranteed
  4. For exceptions, allocate before the try block, then use finally to deallocate — don’t put allocation inside try if deallocation is in finally

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

  1. You can’t write perfect software — build defenses against your own mistakes, not just others’.
  2. DBC makes contracts explicit: preconditions (caller’s duty), postconditions (routine’s guarantee), invariants (class’s promise).
  3. Crash early — a dead program does less damage than one that continues in an invalid state.
  4. Assert the impossible — leave assertions on in production; that’s where the impossible happens.
  5. Whoever allocates a resource owns its deallocation; use scoped lifetime features whenever available.
  6. Take small steps with frequent feedback; don’t try to see further than your headlights reach.