books / clean-code / ch-05SHEET 5 / 12 · REV ASIGN IN
MODULE 5 · CLEAN CODE CH 7

Errors Without the Clutter

Failure deserves to be a first-class citizen of your code, not a swarm of checks that buries the logic you came to read.

The idea

Open a battle-worn codebase and count the lines. Half of them aren't doing work — they're checking whether the previous line worked. A check after every call, a null guard around every use. Error handling matters, but when it obscures the logic, it's wrong.

Return codes are the root of that clutter. When a function signals failure by returning -1 or null, every caller inherits a chore the compiler will never remind them about, and one missed check lets the error travel silently until it detonates three functions away. Exceptions separate the two paths: the algorithm lives in the try, the recovery lives in the catch, and an unhandled failure stops where it happened. A useful discipline is to write the try-catch first, defining the transaction and its abort behavior, then fill in the middle.

An exception is only useful if it carries its whole story. At 2 a.m. you want to read what operation was attempted, on what input, and why it couldn't continue — not the word "failed". And define exception classes by what the caller needs to distinguish, not by where the error came from. If every failure of a third-party library gets handled the same way, one wrapper class is right, not seven.

Some failures aren't failures at all. Customer not found, this month's expense file absent — instead of returning null and making thirty callers branch, return an object that is the answer for that case: a MissingCustomer that renders as "(unknown)", an empty list instead of null. That's the Special Case pattern, and it's the practical form of the rule this module enforces: inside your own code, null never crosses a function boundary. Null may exist at the very edge where outside data arrives, and it dies there.

The bench — 4 exercises

EX 01

Failure-Path Net

Before touching the messy import pipeline, write tests that pin down how it fails today — so the refactor can be proven not to change anything.

  1. Take a small file-import pipeline (read, parse, validate, persist to SQLite) whose failure behavior is inconsistent: a -1 here, a null there, one swallowed exception.
  2. Write five failure tests: missing file, malformed row, validation failure, duplicate id, and a database write failure (force it by locking the db file).
  3. Assert only on observable outcomes — exit status, the summary line the CLI prints, the rows that actually landed in the db.
  4. Run them against the untouched code and make all five pass before you change a single line.
  5. Sabotage each of the five failure sites in turn and confirm at least one test goes red for each.
hint

If a test can't pass against the old code, you're asserting how the error is reported rather than what the program does about it. Assert one level up.

hint

These tests must survive the refactor untouched — that survival is the whole point.

DONE WHEN

· All five failure tests pass against the original, unmodified code

· Each of the five failure sites, when sabotaged, turns at least one test red

· The existing happy-path tests still pass

EX 02

The Return-Code Exorcism

Convert an entire error path from numeric codes and out-of-band error strings to a small, purposeful exception hierarchy — with the story attached.

  1. Inventory every failure signal: numeric returns, a shared mutable lastError string, and the call sites that check them (including the one that forgets).
  2. Define exception classes by caller need — typically just two, like RowError (report and continue) and ImportAborted (stop everything), each carrying file, row, operation and cause.
  3. Write the try-catch structure at the top of the import transaction first, then unwind the check sites beneath it.
  4. Wrap the SQLite driver's error types at the persistence boundary so no vendor error type escapes your db module.
  5. Delete the last translation shim and confirm nothing returns an error code anymore.
hint

Go strangler-style: raise exceptions at the deepest function and have the old wrappers translate exception back to code so tests stay green between steps.

hint

Grep your source for -1, 0, 1 returns and for lastError — survivors should be zero.

hint

Two classes is usually enough. Seven means you invented distinctions no caller acts on.

DONE WHEN

· Grep finds zero numeric error-code returns and zero out-of-band error state in the source

· Every triggered failure produces a message containing the file name and, where relevant, the row number

· SQLite driver error types appear only inside the persistence module

· The five failure tests and the original happy-path tests are all still green

EX 03

The Null Hunt

Drive every return null, null check, and nullable parameter out of the codebase — replacing them with Special Case objects and empty collections.

  1. List every null return, null check, and nullable parameter in the source; that list is your worklist.
  2. Replace missing-customer nulls with a MissingCustomer object that reproduces exactly what the old null-checking callers did — the "(unknown)" name, the empty order history.
  3. Return an empty collection instead of null for an absent monthly expense file.
  4. Fix nullable parameters by splitting the function or making the caller pass a real value.
  5. Allow one boundary file where raw outside data enters — convert to a Special Case or exception there, and let nothing null travel past it.
hint

A Special Case object isn't a dummy — it's the real answer for that case. Ask what all the null-checking callers actually did, and put exactly that inside the object.

hint

If two callers did different things with the null, you've found two special cases, or one caller that was wrong.

DONE WHEN

· Zero null/None returns and zero null literal arguments outside the single boundary file

· At most one null check remains, at the conversion point

· The unknown-customer report renders byte-identically to before the refactor

· All previous tests still green

EX 04

Crash Autopsy

A timed diagnosis drill that measures, on your own clock, what context-rich exceptions are actually worth.

  1. Keep a copy of the module-start build (return codes) alongside your finished build.
  2. Run a deliberately broken input bundle through each and, from the output alone, write down which row of which file failed and why — no source-diving until you've committed to a guess.
  3. Record both answers and both diagnosis times in a findings file.
  4. Strip the row number out of your own exception messages, rerun, and notice the advantage evaporate.
hint

The return-code build tells you that it failed; a good exception tells you what happened.

hint

If your time on your own build isn't dramatically shorter, your messages need more story.

DONE WHEN

· You correctly identify the failing row and cause for both builds

· Your recorded diagnosis time on the refactored build is clearly shorter

· Stripping context measurably slows you down on a rerun

Go deeper (after the bench)

Read Clean Code Chapter 7 (Error Handling, pp. 103-112) now — the DeviceController and wrapped ACMEPort example is exercise two in miniature, and the Special Case discussion is the blueprint for exercise three. If you want one more thing, watch Tony Hoare's free QCon talk "Null References: The Billion Dollar Mistake" — forty minutes of the inventor of null explaining why he regrets it, which makes the zero-null rule feel conservative.