Clean Code advises writing the try-catch-finally block first and treating error handling as "one thing". What does that mean in practice, and how does it relate to fail-fast and resource cleanup?
answer
- Try defines a transaction-like scope — design it before the body
- TDD the failure: expecting-exception test first
- try is first word; nothing after catch/finally
- Fail fast on your invariants; degrade at the edge
- Prefer try-with-resources/defer/RAII; never return from finally
basics
~20 sWrite the failing case first — start with a test that expects the exception, then a try/catch/finally skeleton, then fill in the happy path. A function that handles errors should do only that: try is the first statement, and there's nothing after the catch/finally block.
solid answer
~50 sTwo related disciplines. **Try first**: because a `try` block defines a scope that may abort at any point, deciding upfront what state must be consistent afterwards (and what must be cleaned up) is easier than retrofitting handling into finished code. Test-driving the failure — write a test that expects the exception, watch it fail, then build the handling — keeps the failure path as designed and covered as the happy one. **Error handling is one thing**: if a function contains a try/catch, that should be its whole body. `try` is the first word, `catch`/`finally` end the function, and the real work moves into a called function. This keeps functions small, makes the recovery policy readable in isolation, and prevents the algorithm from being interleaved with recovery code. **Fail fast** complements both: validate preconditions at entry and abort immediately rather than continue on invalid state. Cleanup belongs in `finally` (or RAII/`defer`/try-with-resources) so it survives every exit path.
code
pseudocode · 17 lines// error handling is the function's ONE thing
void delete(Page page) {
try {
deletePageAndAllReferences(page); // the algorithm lives elsewhere
} catch (StorageException e) {
logError(e); // the policy lives here
}
}
// cleanup bound to acquisition, correct on every exit path
try (Connection c = pool.open(); Statement s = c.prepare(sql)) {
s.execute();
} // closed on success, on throw, and on early return
// DON'T: finally that swallows the real failure
try { doWork(); }
finally { return DEFAULT; } // discards any in-flight exceptiongo deeper
Say: write the try/catch/finally skeleton (and a test that expects the exception) before the body; put cleanup in finally; never leave an empty catch.
Add 'error handling is one thing' — try first, nothing after catch — and prefer try-with-resources/using/defer over hand-written finally; explain fail-fast precondition checks.
Discuss exception-safety guarantees (basic/strong/no-throw), suppressed exceptions during cleanup, why multi-step operations need transactions or compensation, and the narrow-catch + single-boundary-handler pattern.
Frame it as a system property: where the failure domains are, fail-fast on invariants vs graceful degradation at the edge, idempotency and compensation for partial failure, and organisation-wide handler/observability conventions so no failure is silent.
## "Write your try-catch-finally statement first" The reasoning is about **scope and invariants**. A `try` block behaves a bit like a transaction: execution can leave it at any statement, so the code after it must be correct for *every* partial execution. That is a design constraint, and constraints are cheapest to satisfy before the code exists. If you write 40 lines of happy path and only then ask "what if line 17 throws?", you usually discover that lines 1–16 left a file open, a lock held, or a half-updated object. The practical loop, which is just TDD applied to failures: 1. Write a test that expects the failure ("reading a missing file throws StorageException"). 2. Watch it fail. 3. Add the try/catch/finally skeleton — a stub is fine. 4. Make the failure test pass. 5. Then fill in the happy path, with the error scope already defined. The payoff is that error paths get *tests*, which is precisely where untested code usually lives — the catch block that has never once executed in CI is a classic source of production surprises (the handler itself throws, logs a null, or references an out-of-scope variable). ## "Error handling is one thing" Clean Code's rule that functions should do one thing applies to handling too. If a function has a try/catch, the handling *is* its one thing: ``` void delete(Page page) { try { deletePageAndAllReferences(page); // the work: one call } catch (Exception e) { logError(e); // the policy } } ``` `deletePageAndAllReferences` contains the algorithm and knows nothing about recovery; `delete` contains the recovery and knows nothing about the algorithm. You can read either without the other. This also makes the *policy* explicit and reviewable — "this operation is best-effort and we only log" is a decision, and it's now visible on one screen instead of buried in a 60-line method. A structural consequence: nothing should follow the catch/finally block. Trailing code after a catch is usually cleanup or continuation that belongs either inside `finally` or in the caller. ## Fail fast Fail fast means: **detect an invalid state at the earliest possible point and stop**, rather than continuing and producing wrong output. Concretely — validate arguments and preconditions at the top of a public method; validate configuration at startup, not on first use at 3am; reject malformed input at the system edge; make invalid states unrepresentable in types where you can. Why it's a *clean code* concern and not just robustness: the distance between the cause of a defect and its symptom is the single biggest driver of debugging cost. A bad config value that crashes at boot names itself; the same value that produces a subtly wrong number three services downstream costs days. The counterweight is that fail-fast applies to *your own* invariants, not to everything. At a system boundary handling untrusted traffic, one bad request must not take down the process — there you fail fast on the *request* (reject it) while the service stays up. Fail-fast and graceful degradation are complementary at different scopes: fail fast within a unit of work, degrade gracefully across units. ## Cleanup: finally and its better alternatives `finally` runs on normal exit, on exception, and (in most languages) on `return` from within `try`. It is where you release what you acquired: files, sockets, locks, transactions, temp state. Pitfalls: - **A `return` or `throw` inside `finally` swallows the in-flight exception.** The original failure disappears and you get the finally-block's outcome instead. Never do it. - **An exception thrown while cleaning up** can mask the primary one. Languages with try-with-resources record it as a *suppressed* exception; hand-rolled cleanup usually just loses it. - **Nested acquire/release** by hand nests deeply and is easy to get wrong under partial failure. Prefer the language's scoped mechanism: try-with-resources / `using` / `with` / `defer` / RAII destructors. They bind cleanup to the acquisition site, so it cannot be forgotten and is correct on every exit path — a strictly better expression of the same idea, and dramatically less code than hand-written `finally` chains. ## Related: exception safety A useful framing from C++ but applicable everywhere — what does an operation guarantee if it throws? - **No guarantee**: state may be corrupt. Unacceptable for anything shared. - **Basic guarantee**: invariants hold, no leaks, but state may have changed. - **Strong guarantee**: the operation is atomic — either it fully succeeded or nothing changed (do the work on a copy, then swap; or use a transaction). - **No-throw**: it cannot fail (needed for destructors, cleanup, swap). Deciding which guarantee a method offers is exactly the "write the try block first" question in disguise. Note the multi-step case: three writes to three systems inside one try is not atomic just because you caught the exception — you need a transaction, a compensating action (saga), or an idempotent retry. ## Anti-patterns to name - `catch (Exception e) { }` — the empty catch; silent failure, the exact opposite of fail fast. - Catching a broad type when you only meant to handle one, thereby swallowing programming errors and cancellation/interrupt signals. - Catching, logging, and continuing as if nothing happened, leaving the object half-updated. - Using exceptions to control normal flow, so `catch` blocks carry business logic. - Swallowing an interrupt/cancellation signal without restoring the flag or aborting — the operation becomes uncancellable.
- What's wrong with `catch (Exception e) { log(e); }` as a default?It catches everything — including programming errors, cancellation/interrupt signals and errors you have no strategy for — then continues execution on possibly-corrupt state. Catch the narrowest type you can actually handle; let the rest propagate to a single boundary handler that decides the fate of the whole operation.
- How do you keep the strong (atomic) guarantee across three external writes inside one try block?You can't get it from try/catch alone. Options: a real transaction if all three share one resource manager; otherwise make each step idempotent and retry, or add compensating actions (saga) so a partial failure is unwound. Catching the exception only tells you it broke — it doesn't undo the first two writes.
- Doesn't 'fail fast' conflict with keeping a service available?No — they operate at different scopes. Fail fast on the unit of work (reject the bad request, abort the corrupt transaction, refuse to boot on invalid config) while the process as a whole stays up and degrades gracefully. The rule is: never continue computing on state you know is invalid.
A surgeon lays out the emergency kit and closing protocol before the first incision, not after complications start. The try/catch/finally skeleton is that kit: decide what must be cleaned up and who is called if it goes wrong, then begin the operation.
saying these in an interview costs you the question
- Empty catch blocks, or catch blocks whose only content is a TODO
- `return` or `throw` inside finally, discarding the original exception
- Adding try/catch after the fact, so cleanup and partial-state questions were never designed for
- No tests that exercise the catch path — handlers that have never run
- Catching broad types (Exception/Throwable/bare except) as a default habit
- Assuming a try/catch makes a multi-step operation atomic
- Swallowing cancellation/interruption without re-signalling it