Checked vs unchecked
The hierarchy, the original intent, why most modern code prefers unchecked, and the rule that decides it for you.
Java is the only mainstream language with checked exceptions, and the debate about whether that was a good idea has run for twenty-five years. You do not need to settle it. You need to know the hierarchy, what each half was intended for, and a rule that tells you which to throw — because the wrong choice spreads throws clauses through code that cannot do anything with them, or hides a failure the caller needed to see.
The hierarchy
Throwable
├─ Error JVM-level: OutOfMemoryError, StackOverflowError — do not catch
└─ Exception
├─ RuntimeException UNCHECKED: NullPointerException, IllegalArgumentException,
│ IllegalStateException, UnsupportedOperationException ...
└─ (everything else) CHECKED: IOException, SQLException, InterruptedException ...A checked exception must be declared in the method's throws clause or caught; the compiler enforces it. An unchecked exception (a RuntimeException or Error) propagates silently. Both unwind the stack the same way at run time; the difference is entirely in what the compiler makes you write.
The original intent
Checked: recoverable conditions the caller should plan for — the file is not there, the connection dropped. Unchecked: programming errors — a null where none was allowed, an index out of range, a state the object was never supposed to be in. The idea was that a caller can do something about a missing file but nothing about a bug.
Why modern code prefers unchecked
In practice, most callers of readConfig() throws IOException cannot do anything about the IOException either. So they wrap it, or declare it, and the throws climbs the call stack through layers that have no interest in files. Lambdas made it worse: Function cannot throw checked exceptions, so every stream over I/O needs a wrapper. Spring wraps SQLException into unchecked DataAccessException for exactly this reason; Hibernate, Jackson and most modern libraries throw unchecked.
The cost of unchecked is that the compiler no longer tells a caller what can fail. Documentation and types (Optional, a sealed Result) take over that job.
The rule
Throw unchecked, unless the immediate caller can reasonably recover and you want the compiler to force them to think about it.
That second case is rare and real: InterruptedException (the caller must restore the interrupt flag or stop), a parse failure in a library whose whole purpose is parsing, a retryable I/O failure at the one boundary that retries. Everywhere else — validation, not-found, illegal state, wrapped infrastructure failures — throw a RuntimeException subclass with a good message.
Standard exceptions to reuse
| Situation | Throw |
|---|---|
| Bad argument | IllegalArgumentException |
| Null argument | NullPointerException via Objects.requireNonNull |
| Object in wrong state for this call | IllegalStateException |
| Operation not supported here | UnsupportedOperationException |
| Index problems | IndexOutOfBoundsException |
| I/O wrapped as unchecked | UncheckedIOException |
Use these before inventing your own; a reader knows what they mean.
Catching
- Catch the narrowest type that you can actually handle.
catch (Exception e)at a service layer swallowsNullPointerExceptions that were bugs. - Never catch and ignore. An empty
catchblock is a bug silencer. At minimum log with the exception object (not just its message), and say why it is safe to continue. - Wrap, do not replace.
throw new ServiceException("could not load order " + id, e)keeps the cause.throw new ServiceException("failed")throws away the stack trace that would have explained it. - Do not catch
Error, except at a top-level boundary to log and die. InterruptedException: either rethrow, orThread.currentThread().interrupt()before handling. Swallowing it makes the thread un-cancellable.
Multi-catch and rethrow
try {
parse(input);
} catch (NumberFormatException | DateTimeParseException e) {
throw new ValidationException("bad input: " + input, e);
}One handler for several types with no common useful supertype. A rethrown exception in a catch (Exception e) block keeps its precise checked type for the throws clause (precise rethrow, Java 7).
Exceptions are not control flow
Throwing is expensive — filling in the stack trace walks every frame — and a method that throws on an expected outcome ("no user with that email" on a login form) is slow and lies about what happened. Return Optional, a boolean, or a sealed result for outcomes that are part of the normal path. Throw for things that should not happen or that the caller cannot continue past.