The Throwable hierarchy: checked vs unchecked
How Java classifies exceptions — and why the checked/unchecked distinction matters.
The exception hierarchy
Every problem Java can throw is a subclass of Throwable:
Throwable
├── Error ← don't catch (JVM-level: OutOfMemoryError, StackOverflowError)
└── Exception
├── RuntimeException ← unchecked
│ ├── NullPointerException
│ ├── IllegalArgumentException
│ ├── ClassCastException
│ └── ...
└── IOException ← checked
└── SQLException ← checked
└── ... (checked)
Checked exceptions — everything that extends Exception but NOT RuntimeException. The compiler enforces the handle-or-declare rule: if a method can throw a checked exception, callers must either catch it or declare it with throws.
Unchecked exceptions — RuntimeException and its subclasses, plus Error. The compiler doesn't force you to handle them. They signal programming errors (NullPointerException, ArrayIndexOutOfBoundsException) or unrecoverable JVM failures (Error).
The basic try/catch/finally structure:
try {
String text = Files.readString(Path.of("data.txt")); // throws IOException
System.out.println(text);
} catch (IOException e) {
System.err.println("Read failed: " + e.getMessage());
} finally {
System.out.println("this always runs");
}
finally runs regardless: whether the try completes normally, an exception is caught, or an exception is uncaught and propagates. The only exceptions are JVM abort (System.exit) and Error-level crashes.