Exception Handling

The Exception Hierarchy

15 min Lesson 3 of 14

The Exception Hierarchy

Every error signal in Java — whether a program bug, a missing file, or an out-of-memory crash — is represented as an object. All of those objects belong to a single inheritance tree rooted at the class Throwable. Understanding this tree tells you which problems you are expected to handle, which ones indicate bugs in your code, and which ones are so serious that recovery is nearly impossible.

The full picture

Here is the hierarchy you need to memorise:

java.lang.Object └── java.lang.Throwable ├── java.lang.Error │ ├── OutOfMemoryError │ ├── StackOverflowError │ └── AssertionError (and others) └── java.lang.Exception ├── IOException ├── SQLException ├── ParseException └── java.lang.RuntimeException ├── NullPointerException ├── ArrayIndexOutOfBoundsException ├── IllegalArgumentException ├── NumberFormatException └── ClassCastException (and others)

Every class you see in a stack trace is somewhere in this tree. Let us go through each level.

Throwable — the root of everything

Throwable is the superclass of all throwable objects. Only instances of this class (or its subclasses) can be used with throw, catch, or throws. It defines the core methods you use every day:

  • getMessage() — returns the human-readable error message.
  • getCause() — returns the exception that caused this one (used for chained exceptions).
  • printStackTrace() — prints the full call stack to stderr.
  • getStackTrace() — returns the call stack as an array you can inspect programmatically.
You almost never extend Throwable directly. You extend either Exception (for recoverable situations) or RuntimeException (for programming mistakes). Extending Throwable itself bypasses the checked/unchecked distinction and surprises other developers.

Error — do not catch these

Error represents a serious problem that a well-written application should not try to catch. These are typically JVM-level failures that your code has no realistic way to recover from.

  • OutOfMemoryError — the JVM ran out of heap space. There is nothing useful you can do once this fires.
  • StackOverflowError — infinite or too-deep recursion exhausted the call stack.
  • AssertionError — an assert statement failed during testing.
// This method will throw StackOverflowError — do NOT do this public static int infiniteRecursion(int n) { return infiniteRecursion(n + 1); // no base case — the stack fills up and crashes }
Do not catch Error or its subclasses in production code. A common beginner mistake is writing catch (Exception e) thinking it catches everything — it does not catch Error. Even catch (Throwable t) will catch Errors, but that is almost always wrong. Log the situation and let the JVM (or your container) shut down cleanly.

Exception — the recoverable branch

Exception is the class for conditions that a reasonable program might want to catch and handle. Most of the classes you write try/catch blocks for live here. Direct subclasses of Exception (other than RuntimeException) are called checked exceptions — the compiler forces you to either catch them or declare them with throws.

import java.io.FileReader; import java.io.IOException; public class ReadDemo { public static void main(String[] args) throws IOException { // IOException is a checked Exception — the compiler insists we declare it FileReader reader = new FileReader("data.txt"); System.out.println("File opened successfully"); reader.close(); } }

If you omit throws IOException and do not wrap the call in a try/catch, the code will not compile. That is the compiler protecting you: it knows this operation might fail and forces you to think about it.

RuntimeException — programming mistakes

RuntimeException is a special subclass of Exception. Its subclasses are called unchecked exceptions — the compiler does not require you to handle them. They typically indicate bugs in your code: you passed null where an object was expected, you accessed an array index that does not exist, or you tried to cast an object to the wrong type.

public class HierarchyDemo { public static void main(String[] args) { // NullPointerException — unchecked, no compiler warning String text = null; System.out.println(text.length()); // throws NullPointerException at runtime // ArrayIndexOutOfBoundsException — unchecked int[] numbers = {10, 20, 30}; System.out.println(numbers[5]); // throws ArrayIndexOutOfBoundsException // NumberFormatException — unchecked int value = Integer.parseInt("abc"); // throws NumberFormatException } }
Fix the bug, do not catch it. When you see a NullPointerException or ArrayIndexOutOfBoundsException you should fix the code that caused it — add a null check, validate the index — rather than swallowing it in a catch block and hiding the bug.

Checking the hierarchy at runtime with instanceof

Because the hierarchy uses ordinary inheritance, you can use instanceof to inspect where a thrown object sits in the tree:

import java.io.IOException; public class InstanceofDemo { public static void main(String[] args) { Throwable ex = new IOException("file not found"); System.out.println(ex instanceof Throwable); // true System.out.println(ex instanceof Exception); // true System.out.println(ex instanceof IOException); // true System.out.println(ex instanceof RuntimeException); // false System.out.println(ex instanceof Error); // false } }

This also explains why a single catch (Exception e) block catches both checked exceptions like IOException and unchecked ones like NullPointerException — they are both subclasses of Exception. But it will not catch an OutOfMemoryError, because Error is a sibling branch, not a subclass of Exception.

Why the hierarchy matters in practice

  • Catching a superclass catches all subclasses. catch (Exception e) will catch IOException, SQLException, NullPointerException, and thousands more. Be as specific as possible so different problems get different handling.
  • The compiler enforces checked exceptions. Any class that is a direct subclass of Exception (but not of RuntimeException) must be declared or caught. The hierarchy is what makes this rule possible.
  • Custom exceptions slot into the right place. When you create your own exception class (covered in a later lesson), you choose which class to extend — and that choice determines whether your exception is checked or unchecked.

Summary

Throwable is the root. Error signals JVM failures you should not catch. Exception signals recoverable problems — its direct subclasses are checked (compiler-enforced), while subclasses of RuntimeException are unchecked (indicating bugs). Every class in a stack trace lives in this tree, and that position determines how Java and the compiler treat it.