PrepZone Logo
PrepZone

The Exception Hierarchy

Checked versus unchecked, Error versus Exception, and which branch you should never catch.

Read these first

Why this matters

  • The checked-versus-unchecked distinction decides whether the compiler forces a caller to deal with your failure, which is an API design decision rather than a technical one.
  • Catching the wrong branch of the hierarchy turns a recoverable problem into a hidden one, or a fatal problem into a corrupted program that keeps running.
  • Reading a stack trace correctly is the single highest-value debugging skill, and most people read it in the wrong direction.

The hierarchy

Throwable
ErrorOutOfMemoryError — do not catch
ExceptionChecked — compiler enforced
RuntimeExceptionUnchecked — your bugs

RuntimeException sits under Exception but escapes the checked rule — that single exception to the rule is what the whole checked/unchecked distinction rests on.

Everything you can throw descends from Throwable. Only the checked branch forces you to declare or handle it; Error and RuntimeException do not.
  • Throwable is the root. Only its subtypes may appear in a throw or a catch.
  • Error signals a condition outside your program's control — the heap is exhausted, a stack overflowed, a class is missing. Do not catch these.
  • Exception signals an application-level problem. Everything here is fair game to handle.
  • RuntimeException, a subclass of Exception, is the unchecked branch. The compiler does not require handling it.
  • Every other Exception subclass is checked, and the compiler insists you either catch it or declare it.

Checked versus unchecked

The compiler's rule is purely structural: is the type a RuntimeException or an Error? If not, it is checked.

AspectCheckedUnchecked
ExamplesIOException, SQLException, ClassNotFoundExceptionNullPointerException, IllegalArgumentException, IllegalStateException
Compiler requires handlingYes — catch or declareNo
SignalsAn expected external failureA programming error, usually
Caller can recoverOften — retry, fall back, reportRarely — the code needs fixing
Appears in the signatureMust, via throwsOptional, for documentation
  • Examples

    CheckedIOException, SQLException, ClassNotFoundException
    UncheckedNullPointerException, IllegalArgumentException, IllegalStateException
  • Compiler requires handling

    CheckedYes — catch or declare
    UncheckedNo
  • Signals

    CheckedAn expected external failure
    UncheckedA programming error, usually
  • Caller can recover

    CheckedOften — retry, fall back, report
    UncheckedRarely — the code needs fixing
  • Appears in the signature

    CheckedMust, via throws
    UncheckedOptional, for documentation

The intent is that checked means 'something outside went wrong' and unchecked means 'the code is wrong'.

Java
// Checked: the compiler will not let you ignore it
public String readConfig(Path path) throws IOException {
    return Files.readString(path);
}

// Unchecked: no declaration required, though documenting it is good practice
public void setAge(int age) {
    if (age < 0) {
        throw new IllegalArgumentException("Age cannot be negative: " + age);
    }
    this.age = age;
}

Common exceptions and what they really mean

Unchecked — nearly always a bug in your code

  • NullPointerException — a method or field was accessed on null. Since Java 14 the message names the exact expression, which makes this far easier to fix.
  • IllegalArgumentException — a caller passed a value the method cannot accept. The right choice for argument validation.
  • IllegalStateException — the method is valid but not now. Calling next() on an exhausted iterator, for example.
  • IndexOutOfBoundsException — an index outside a collection or array; ArrayIndexOutOfBoundsException is its array-specific form.
  • ClassCastException — a downcast to a type the object is not.
  • NumberFormatException — a subclass of IllegalArgumentException, thrown by Integer.parseInt on unparseable text.
  • ConcurrentModificationException — a collection was structurally modified while being iterated.
  • ArithmeticException — integer division by zero. Floating-point division gives Infinity instead.
  • UnsupportedOperationException — the operation exists on the type but not on this implementation, as with List.of(...).add(...).

Checked — the outside world failed

  • IOException — any failed input or output. FileNotFoundException is the common specific form.
  • SQLException — a database rejected or could not complete the statement.
  • ClassNotFoundException — a reflective lookup by name found nothing on the classpath.
  • InterruptedException — a waiting thread was asked to stop waiting. Never swallow this one.

Errors — do not catch

  • OutOfMemoryError — the heap or metaspace cannot satisfy an allocation.
  • StackOverflowError — usually unbounded recursion.
  • NoClassDefFoundError — a class present at compile time is missing at run time; almost always a packaging problem.
  • ExceptionInInitializerError — a static initialiser threw, so the class can never be used.

Reading a stack trace

Java
Exception in thread "main" java.lang.IllegalStateException: Cart is empty
	at com.prepzone.cart.Cart.checkout(Cart.java:47)
	at com.prepzone.order.OrderService.place(OrderService.java:23)
	at com.prepzone.web.OrderController.submit(OrderController.java:15)
Caused by: java.sql.SQLException: Connection timed out
	at com.prepzone.db.ConnectionPool.borrow(ConnectionPool.java:88)
	... 3 more

How to read it

  • The first line is the exception type and message — what went wrong.
  • The topmost at line is where it was thrown. That is where you look first, not the bottom.
  • Lines below are the call chain that led there, most recent first.
  • Caused by is the original failure, wrapped by the one above. The lowest Caused by is usually the real root cause.
  • ... 3 more means the remaining frames are identical to the enclosing trace and were elided.

Wrapping preserves the cause

When you translate an exception into one appropriate for your layer, always pass the original as the cause.

Java
public Order findById(String id) {
    try {
        return repository.load(id);
    } catch (SQLException e) {
        // Right: the original cause travels with it
        throw new OrderLookupException("Could not load order " + id, e);
    }
}
Java
// Wrong: the SQLException and its stack trace are gone forever
catch (SQLException e) {
    throw new OrderLookupException("Could not load order " + id);
}

The second version produces a stack trace that points at your own code and tells you nothing about the timeout, the constraint violation or the deadlock that actually happened. This is the most expensive single mistake in exception handling, because it destroys the information at the exact moment you will need it.

Throwing well

Java
public void transfer(Account from, Account to, BigDecimal amount) {
    Objects.requireNonNull(from, "from account is required");
    Objects.requireNonNull(to, "to account is required");

    if (amount.signum() <= 0) {
        throw new IllegalArgumentException("Transfer amount must be positive, was " + amount);
    }
    if (from.balance().compareTo(amount) < 0) {
        throw new InsufficientFundsException(from.id(), amount, from.balance());
    }
}
  • Fail fast. Validate at the entry point, before any state changes.
  • Put the offending value in the message. "Invalid amount" costs a debugging session; "Transfer amount must be positive, was -50" does not.
  • Choose the most specific type available. IllegalArgumentException for a bad argument, IllegalStateException for bad timing.
  • Use Objects.requireNonNull for null checks; it throws a properly worded NullPointerException in one line.

Common misreadings

  • "Unchecked exceptions cannot be caught." They can be caught exactly like checked ones. The difference is only that the compiler does not insist.
  • "Error and Exception are related by inheritance." Both extend Throwable, but neither extends the other.
  • "NullPointerException is checked." It is a RuntimeException, so unchecked.
  • "Catching Exception is a reasonable default." It hides bugs you wanted to see. Catch what you can actually handle.
  • "The bottom of a stack trace is the error." The top frame is where it was thrown; the deepest Caused by is the root cause.
  • "Throwing is expensive, so avoid exceptions." Filling in the stack trace costs something, which is why exceptions should not be control flow — but a genuine error path is exactly what they are for.

Quick recall

Everything you need if you only revisit this box.

  • Throwable splits into Error (JVM-level, do not catch) and Exception (application-level).
  • RuntimeException and its subclasses are unchecked; every other Exception is checked and must be caught or declared.
  • Checked means "the outside world may fail and you can recover"; unchecked means "the code is wrong".
  • Never catch (Throwable) — it swallows Error.
  • Read a trace top down: first at line is the throw site, the deepest Caused by is the root cause.
  • Always pass the original exception as the cause when wrapping. Dropping it destroys the evidence.
  • Fail fast, include the offending value in the message, and pick the most specific exception type.

Test yourself

Answer these before moving on — recall is what makes it stick.