Checked vs Unchecked Exceptions in Practice
Java is the only mainstream language that kept checked exceptions, and the debate about whether that was a good idea is thirty years old. What matters day to day is narrower: which one to throw, and what to do when you catch one.
The rule the compiler enforces
Anything extending Exception but not RuntimeException is checked. The compiler requires every caller to either catch it or declare it:
public void save(String data) throws IOException { // checked, must be declared
Files.writeString(path, data);
}
public void withdraw(BigDecimal amount) { // unchecked, nothing to declare
if (amount.compareTo(balance) > 0) {
throw new InsufficientFundsException(amount);
}
}
Error and its subclasses (OutOfMemoryError, StackOverflowError) are also unchecked, and are not yours to catch.
The original intent, and why it fell apart
Checked exceptions were meant for recoverable conditions: the caller can do something sensible about a missing file, so make them think about it.
The trouble is that "the caller" is rarely the code that can recover. A repository throws SQLException, and the thing that can act on it is six frames up in a controller. Every layer in between has to either declare it, which leaks persistence details all the way to the top, or wrap it. So the ecosystem settled on wrapping, and checked exceptions became a tax rather than a design tool.
The visible result of that is throws Exception on a method signature, which tells callers nothing while forcing them to handle everything, and the catch block everyone has written:
try {
doSomething();
} catch (IOException e) {
// TODO
}
A caught and ignored exception is worse than a crash, because the program continues in a state you did not design.
What Spring did
Spring translates every SQLException and every JPA provider exception into its own unchecked hierarchy under DataAccessException. That is why repository methods have no throws clause and why you can catch DuplicateKeyException without knowing whether you are on Postgres or H2.
Transactions follow the same split, and this one surprises people: @Transactional rolls back on unchecked exceptions by default, and commits on checked ones.
@Transactional
public void transfer() throws InsufficientFundsException {
debit();
throw new InsufficientFundsException(); // checked: the debit is committed
}
If you throw checked exceptions from transactional methods, say so explicitly:
@Transactional(rollbackFor = InsufficientFundsException.class)
This is worth checking alongside the other reason transactional code silently misbehaves, covered in why your @Transactional method is being ignored.
Wrapping without destroying evidence
When you do wrap, pass the cause:
try {
return mapper.readValue(json, Order.class);
} catch (JsonProcessingException e) {
throw new InvalidOrderPayloadException("Could not parse order", e);
}
That second argument is the difference between a stack trace that shows the parse failure and one that starts at your own throw statement. The version without it is the single most common way debugging information is lost in Java code.
Two related habits. Never log and rethrow, because you get the same failure twice in the log and the second copy has less context. And never catch an exception just to e.printStackTrace(), which writes to stderr, bypasses your log configuration and is invisible in most production setups.
Never swallow InterruptedException
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// ignored
}
Catching this clears the thread's interrupt flag, so the cancellation signal is gone and nothing above knows the thread was asked to stop. Either propagate it, or restore the flag:
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while waiting", e);
}
What to throw in your own code
Default to unchecked. Extend RuntimeException, give the exception a name that describes the domain condition, and let it travel to the layer that can turn it into a response, as in exception handling in Spring Boot.
Use a checked exception only when the immediate caller has a genuine alternative path and you want the compiler to insist they take it. That is rarer than it sounds, and in a typical web application it is close to never.
Two smaller rules that pay for themselves. Put the useful values in the message ("No order with id " + id, not "Not found"), because that string is often all you will have at three in the morning. And do not catch Exception anywhere except the one central handler at the top, where catching everything is the actual job.