Java Exception Hierarchy
How Throwable, Error, Exception, and RuntimeException relate in Java's exception class hierarchy.
Java Exception Hierarchy
Every exception in Java is part of a small class tree rooted at java.lang.Throwable. Knowing the shape of that tree pays off constantly: it explains why catch (Exception e) doesn't catch OutOfMemoryError, why RuntimeException is special, and why some exceptions force you to handle them while others don't. The whole layout fits in one diagram.
The whole tree
Throwable
├── Error (unchecked — JVM-level)
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ ├── VirtualMachineError
│ └── ...
└── Exception (checked by default)
├── IOException (checked)
├── SQLException (checked)
├── ClassNotFoundException (checked)
├── ...
└── RuntimeException (unchecked)
├── NullPointerException
├── IllegalArgumentException
├── IndexOutOfBoundsException
├── ArithmeticException
├── ClassCastException
├── IllegalStateException
└── ...The whole tree is one class hierarchy. That's why a catch for a supertype catches its subtypes, why exception variables behave like ordinary references, and why you can hold an IOException in an Exception-typed field.
Throwable — the root
Throwable is what throw accepts and what catch declares. Anything you'd want to raise or handle is a subclass of Throwable. The class itself provides:
- A message (
getMessage()) - A stack trace captured at construction (
getStackTrace(),printStackTrace()) - An optional cause — another
Throwablethat triggered this one (getCause()) - Suppressed exceptions — secondary failures attached by try-with-resources (
getSuppressed())
You almost never extend Throwable directly. The interesting design lives one level below.
Error — don't catch
Error and its subclasses represent failures the JVM signals: out of memory, stack overflow, a class file that can't be linked. By convention you do not catch them in application code, because:
- They usually mean the JVM is no longer reliable. Continuing past an
OutOfMemoryErrorrarely works for long. - There's almost never a sensible recovery action your code can take.
- The JVM itself may already be doing something about them; intercepting interferes.
Error is technically catchable — Java doesn't stop you. But the convention is so strong that "catches Error" is treated as a code-review red flag. The only legitimate use case is a top-level supervisor (a request handler, a job runner) that logs and exits cleanly.
Exception — application failures
Everything other than Error under Throwable is Exception or one of its subtypes. The line between checked and unchecked runs inside this branch, not above it:
- Direct subclasses of
Exceptionthat aren'tRuntimeExceptionare checked. RuntimeExceptionand all its subtypes are unchecked.
That's why catch (Exception e) matches both IOException (checked) and NullPointerException (unchecked) — they're siblings under the same root. It's also why catching Exception is so blunt: you've thrown both branches together.
RuntimeException — the bug branch
RuntimeException and its subtypes are reserved by convention for programming errors that shouldn't happen in correct code:
NullPointerException— dereferencing nullIllegalArgumentException— bad argumentIllegalStateException— wrong state for the operationIndexOutOfBoundsException— list/array index past the endArithmeticException— divide by zeroClassCastException— bad castUnsupportedOperationException— operation not supported (e.g. mutating an unmodifiable list)
You can throw these from anywhere without changing your method signature. Callers may catch them, but the language doesn't make them. They're the right tool when the failure says "this is a bug" rather than "this happens sometimes."
Type relationships in catch
A catch (T e) matches any thrown value that is an instance of T or a subtype of T. So the hierarchy directly dictates what your catches see:
try { ... }
catch (IOException e) { ... } // catches FileNotFoundException too
catch (Exception e) { ... } // catches almost everything below Throwable
catch (Throwable t) { ... } // catches everything, including Error — don'tThis is why a one-catch-all-the-things shape is dangerous. catch (Exception) catches NullPointerException (a bug) and IOException (a recoverable failure) and IllegalStateException (probably a bug) — all in one block, with no way to handle them differently. The hierarchy is asking you to be more specific.
Looking up types
When you encounter a new exception in a stack trace and want to know where it sits:
- It's in
java.langif it's a fundamental error (NullPointerException,ArithmeticException). - It's in
java.io,java.sql,java.netif it relates to that package's domain. - A class ending in
Erroris almost certainly underError. - A class ending in
Exceptionis almost certainly underException— but check whether it extendsRuntimeExceptionto know if it's checked.
The Javadoc shows the inheritance chain at the top of every page. When in doubt, look it up.
A worked example
A small program that walks the hierarchy with instanceof checks. It catches a sequence of throws as Throwable, then reports where each one sits in the tree.
The isChecked helper encodes the rule in one line: the checked subset is Exception minus RuntimeException. Run the program and you'll see exactly which of the five sits where: IOException is checked, the two RuntimeExceptions aren't, the OutOfMemoryError is an Error (so it's neither an Exception nor checked), and the plain Exception is checked.
What's next
The built-in tree covers most cases. When your domain has its own failures — "invoice not found," "config out of date" — you write your own classes. Continue to Java custom exceptions.
Practice
A program does `catch (Exception e)` around a block. Which of these will be caught?