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.
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
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
catch (Exception e) { } // the worst line in any codebase
catch (Exception e) { e.printStackTrace(); } // barely betterThe 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?
| Situation | Response |
|---|---|
| Invalid argument from a programmer | unchecked, it is a bug |
| A file that may legitimately not exist | the caller can handle it |
| Network failure | the caller can retry |
| Out of memory | nobody can handle it |
Exceptions are not control flow
try:
value = int(user_input)
except ValueError:
value = 0Fine, 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.