The Java exception hierarchy is a single tree rooted at Throwable, and it splits into two branches: Error for conditions the runtime cannot usually recover from, and Exception for conditions your program is expected to handle. Where a class sits in that tree decides whether the compiler forces you to catch it.
What is the shape of the tree?
Three facts carry most of the weight. First, Throwable is the only direct child of Object that can be thrown. Second, Error and Exception are siblings, so a catch block for one never catches the other. Third, RuntimeException sits underneath Exception and marks the boundary between the two families people casually call checked and unchecked exceptions.
| Level | Typical members | What it signals |
|---|---|---|
| Throwable | the root of every throwable class | Catching it catches everything, including problems you cannot fix. |
| Error | StackOverflowError, OutOfMemoryError, NoClassDefFoundError | The runtime hit a state it normally cannot recover from. |
| Exception | IOException, SQLException, ParseException | A failure your program can reasonably be expected to handle. |
| RuntimeException | NullPointerException, ArithmeticException, ClassCastException, IllegalArgumentException, IllegalStateException, IndexOutOfBoundsException | Unchecked failures, almost always a mistake in your own code. |
Everything you will meet in day-to-day work lives in the last two rows. The names are less important than the branch they hang from: if a failure can be triggered by the code you wrote rather than by an external system, it belongs under RuntimeException.
How can you see the tree while the program runs?
Every throwable object knows its own class, and every class knows its parent, so you can walk the chain at runtime. This is the fastest way to answer "which family is this in?" without guessing from the exception name.
public class HierarchyDemo {
public static void main(String[] args) {
try {
int result = 10 / 0;
System.out.println(result);
} catch (ArithmeticException e) {
System.out.println("caught type: " + e.getClass().getName());
System.out.println("message: " + e.getMessage());
Class<?> type = e.getClass();
while (type != null) {
System.out.println(" " + type.getSimpleName());
type = type.getSuperclass();
}
}
}
}
caught type: java.lang.ArithmeticException
message: / by zero
ArithmeticException
RuntimeException
Exception
Throwable
Object
Read that chain from the bottom up and the rule becomes obvious: ArithmeticException inherits from RuntimeException, so it is unchecked and the compiler never asked you to declare it. A catch block for RuntimeException would have caught it too, because the catch clause matches the class and every class above it in the chain.
Which failures are unchecked?
The unchecked family is the RuntimeException subtree, and it covers the mistakes beginners make most often: dividing by zero, dereferencing a null reference, reading past the end of a list, casting an object to the wrong type, calling a method on an object in a state that does not allow it. None of these need a throws clause, because no caller can do anything sensible about them.
Catching them is usually a mistake in disguise. A NullPointerException that you swallow with an empty catch block turns a loud crash into a silent wrong answer, which is far harder to track down. When you do catch an unchecked exception, it is nearly always to add context, log it, or convert it into a domain-level failure.
public class ErrorVsExceptionDemo {
public static void main(String[] args) {
Object[] items = { "book", 42, 3.5 };
for (Object item : items) {
try {
String text = (String) item;
System.out.println(text.toUpperCase());
} catch (ClassCastException e) {
System.out.println(item.getClass().getSimpleName()
+ " is not a String -> " + e.getClass().getSimpleName());
}
}
try {
throw new StackOverflowError("simulated deep recursion");
} catch (Error e) {
System.out.println("Error caught: " + e.getClass().getSimpleName());
} catch (Exception e) {
System.out.println("never reached");
}
}
}
BOOK
Integer is not a String -> ClassCastException
Double is not a String -> ClassCastException
Error caught: StackOverflowError
That program also shows the sibling relationship in action. The second catch clause catches Exception, yet it never runs, because StackOverflowError is not a descendant of Exception — it sits on the other branch of the tree.
Should you ever catch an Error?
Rarely, and never as routine practice. An Error usually means the runtime itself is in trouble: the stack overflowed, the heap ran out, a class failed to link. Continuing after such a failure tends to produce corrupted data rather than a working program. The exception is LinkageError in one narrow case: if a class is missing at runtime you can sometimes fall back to a different implementation, and catching that specific error is defensible.
The practical rule is to catch the most specific class you can handle and let anything broader travel up the call stack, where the code that started the operation can decide what to do.
Where do checked exceptions sit in the tree?
They sit directly under Exception, outside the RuntimeException subtree. These describe failures caused by things outside your program — a file that is not there, a socket that dropped, a query that violates a database constraint — and the caller might genuinely be able to do something about them, so the compiler insists you deal with them.
import java.io.IOException;
public class CheckedHierarchyDemo {
public static void main(String[] args) {
Exception e = new IOException("disk offline");
Class<?> type = e.getClass();
while (type != null) {
System.out.println(type.getSimpleName());
type = type.getSuperclass();
}
System.out.println("counted as unchecked? " + (e instanceof RuntimeException));
}
}
IOException
Exception
Throwable
Object
counted as unchecked? false
Notice that RuntimeException does not appear in that chain. The absence is the whole point: a class is unchecked exactly when RuntimeException shows up somewhere above it.
How do you find out what actually threw?
When an exception is created, the runtime records the call stack at that moment. That stack is attached to the exception and travels with it, so even after you catch it several frames away you can still see the exact method that went wrong.
public class StackTraceDemo {
static void levelThree() {
throw new IllegalStateException("state not ready");
}
static void levelTwo() {
levelThree();
}
static void levelOne() {
levelTwo();
}
public static void main(String[] args) {
try {
levelOne();
} catch (IllegalStateException e) {
StackTraceElement[] frames = e.getStackTrace();
System.out.println("frames captured: " + frames.length);
for (StackTraceElement frame : frames) {
System.out.println(" " + frame.getClassName() + "." + frame.getMethodName());
}
}
}
}
frames captured: 4
StackTraceDemo.levelThree
StackTraceDemo.levelTwo
StackTraceDemo.levelOne
StackTraceDemo.main
Frame zero is the method that threw, and the rest are the callers in reverse order, ending with main. Calling getStackTrace() gives you that list as data, and printStackTrace() prints the familiar block of text you see when a program dies. Getting used to reading that block from the top down is worth the effort — the top frame is almost always the answer.
The tree in one paragraph
- Everything throwable descends from
Throwable. ErrorandExceptionare siblings; catching one never catches the other.- Anything under
RuntimeExceptionis unchecked, and the compiler ignores it. - Checked exceptions sit directly under
Exceptionand must be declared or handled. - Every throwable carries a message and the stack trace from the moment it was created.
- Catch the narrowest class that lets you do something useful; let the rest travel upward.
Try it yourself: write a program that throws a java.util.NoSuchElementException, catches it as RuntimeException, and prints the whole class chain from the concrete class up to Object. Then change the catch clause to Exception and confirm the chain does not change. Finally, write a second program that throws an Error and show that a catch block for Exception does not intercept it, while a catch block for Throwable does.