Skip to content

15 · 32 · 5 min

Designing what failure looks like

Knowing the syntax of try and catch is the easy half. Deciding what to throw, where to catch it and what to put in it is the part that makes a system debuggable.

Throw where you know, catch where you can act

The most common mistake is catching too early.

java
try {
    return repository.find(id);
} catch (SQLException e) {
    return null;                 // the caller now has no idea why
}

Returning null discards the reason and moves the failure somewhere distant, where it becomes a null pointer exception with a stack trace pointing at innocent code.

A layer should catch an exception only when it can do something meaningful: retry, fall back, or translate it into terms its own callers understand.

Translating at a boundary

java
try {
    return repository.find(id);
} catch (SQLException e) {
    throw new CustomerLookupFailed("customer " + id, e);   // keep the cause
}

The caller of a customer service should not be catching SQLException; that leaks the storage choice into every layer above. Wrapping it keeps the abstraction and keeps the cause, which is the part people drop.

Never swallow

java
catch (Exception e) { }                 // the worst line in any codebase
catch (Exception e) { e.printStackTrace(); }   // barely better

The first destroys the evidence. The second writes it somewhere nobody is looking and then continues as though nothing happened, so the object is in an unknown state and the failure surfaces later somewhere unrelated.

If an exception genuinely can be ignored, say so in a comment explaining why, because the next reader will otherwise assume it is a bug.

Checked and unchecked

Java's checked exceptions force the caller to handle or declare them. The intent was good and the result is mixed: code full of throws clauses, and enough people writing empty catch blocks to satisfy the compiler that the feature arguably caused more swallowing than it prevented. No language designed since has adopted them.

The useful distinction is not the keyword but the question: can the caller do anything about this?

SituationResponse
Invalid argument from a programmerunchecked, it is a bug
A file that may legitimately not existthe caller can handle it
Network failurethe caller can retry
Out of memorynobody can handle it

Exceptions are not control flow

python
try:
    value = int(user_input)
except ValueError:
    value = 0

Fine, and idiomatic in Python. Using exceptions to exit a loop or to return a normal result is not: they are expensive, they obscure the flow, and a reader cannot follow where control goes.

Put the context in

"Invalid input" is useless at three in the morning. "Order 8823: quantity -5 is not positive" names the record, the field and the rule, and usually means nobody has to reproduce anything.