Module 8 · Exceptions, I/O & Resources ⏱ 19 min

Exception Hierarchy and try/catch/finally

By the end of this lesson you will be able to:
  • Read the Throwable family tree and name the common unchecked exception types
  • Predict which of try, catch, and finally runs in each outcome
  • Order multiple catch clauses so the build does not reject them

When code fails at runtime

Division by zero. Reading past the end of an array. Calling a method on a reference that points at nothing. Each one is a runtime failure, and in a language without exceptions a single such fault would tear the whole program down with no chance to recover.

Java models these failures as objects. When something goes wrong, the JVM constructs an exception object carrying a short message (such as / by zero) and a record of where it happened, then searches for code willing to handle it. Taking part in that search is exception handling, and the try, catch, and finally keywords are how you do it.

The reward is resilience: a program can notice a failure, clean up, and keep serving the next request instead of dying midway through one.

flowchart TD
  T["Throwable"] --> ER["Error<br/>do not catch these"]
  T --> EX["Exception"]
  EX --> RT["RuntimeException<br/>unchecked"]
  RT --> A["ArithmeticException"]
  RT --> N["NullPointerException"]
  RT --> I["IndexOutOfBoundsException"]
  EX --> C["IOException<br/>checked"]
  style RT fill:#c2410c,color:#fff
The Throwable family tree. Error means the JVM is broken; RuntimeException and its subclasses are the everyday unchecked failures.

A family of failure objects

The objects thrown at runtime form a family tree, all descended from Throwable. Knowing the branches tells you what a given catch will and will not match.

One branch is Error, the failures of the JVM itself, such as exhausting memory. You are not expected to catch these; they signal that the environment is broken, not that your method can recover.

The other branch is Exception, and it splits in two. RuntimeException and its subclasses are unchecked: ArithmeticException for impossible maths, NullPointerException for a bare reference, IndexOutOfBoundsException for an out of range position, NumberFormatException for a string that will not parse. The compiler does not force you to guard them, because they usually point at a bug in your own code.

Main.java — a try block that throws, caught and reported, with cleanup in finally. Real, compiling Java.
public class Main {
    public static void main(String[] args) {
        try {
            int result = 10 / 0;            // throws ArithmeticException
            System.out.println(result);    // never reached
        } catch (ArithmeticException e) {
            System.out.println("Caught: " + e.getMessage());
        } finally {
            System.out.println("Done with attempt.");
        }
    }
}

How the three blocks fit together

A try block marks code that might fail, and the JVM runs it statement by statement. If every statement succeeds, control falls straight through the try, skips every catch, runs finally, and continues after the whole construct.

If a statement throws instead, the JVM abandons the try at that exact line — nothing below it runs — and walks down the catch clauses hunting for one whose type matches the exception. The first match wins; its body runs, the exception is considered handled, and the program resumes after the last catch.

finally is the third act and it is unconditional. Whether the try succeeded, threw and was caught, or threw uncaught, finally still runs. That makes it the natural home for cleanup such as closing a file or releasing a lock, work that must happen regardless of outcome.

flowchart TD
  S["enter try block"] --> R{"exception thrown?"}
  R -->|"no"| TRY["run try body to completion"]
  TRY --> FIN["finally runs"]
  R -->|"yes"| MATCH{"a catch matches?"}
  MATCH -->|"yes"| CATCH["run matching catch"]
  MATCH -->|"no"| PROP["exception propagates"]
  CATCH --> FIN
  PROP --> FIN
  FIN --> AFTER["continue after try/catch"]
  style FIN fill:#3776ab,color:#fff
The decision the JVM makes: success skips the catch clauses, a thrown exception is matched against them in order, and finally runs in every case.

The type is the signal

A catch clause does not read the exception's message to decide whether it applies — it matches on type. catch (ArithmeticException e) will catch an ArithmeticException or any of its subtypes, and nothing else. A different runtime failure sails straight past it.

That is why the variable e exists at all: once you have caught the object, you can ask it things. e.getMessage() returns the short text the JVM packed in, such as / by zero, and printing e directly shows the type name together with that message. The type decides whether you catch it; the message tells you what went wrong.

Exceptions travel up the call stack

When no catch in the current method matches, the exception does not simply disappear. It propagates: the JVM unwinds the current method and repeats the search in the method that called it, then that caller's caller, and so on. If the search climbs all the way to main and still finds no handler, the JVM prints the exception together with its stack trace and halts.

That trace — the at ClassName.methodName(File.java:line) lines you will see in every Java error report — is the record of the frames the JVM unwound on its way out. Reading one bottom to top tells you which chain of calls led to the failure, which is most of what debugging a crash amounts to.

Main.java — several catch clauses, ordered specific to broad. The first matching type runs. Real, compiling Java.
public class Main {
    public static void main(String[] args) {
        try {
            String s = null;
            System.out.println(s.length());   // throws NullPointerException
        } catch (NullPointerException e) {
            System.out.println("null reference");
        } catch (RuntimeException e) {
            System.out.println("something else broke: " + e.getMessage());
        } finally {
            System.out.println("cleanup");
        }
    }
}

Several catches, one try

A single try may list several catch clauses, each naming a different type, and the JVM examines them top to bottom. The first clause whose type matches the thrown exception runs, and every clause below it is skipped.

That ordering carries a sharp trap. Because matching works on the type hierarchy, a broad type listed before one of its own subtypes makes the narrower clause unreachable — the broad one would always win first. Java refuses to compile such code, reporting that the subtype has already been caught. Put specific clauses first and broad ones last, and you will never hit that error.

flowchart LR
  EX["throw ArithmeticException"] --> C1["catch RuntimeException<br/>listed first, matches it"]
  C1 -.->|"would also match"| C2["catch ArithmeticException<br/>unreachable: compile error"]
  style C2 fill:#b91c1c,color:#fff
  style C1 fill:#c2410c,color:#fff
A broad catch placed before one of its own subtypes makes the subtype clause unreachable — a compile error, not a silent skip.
Exercise

What does this print? The try body runs without throwing. Type the exact output, four lines.

public class Main {
    public static void main(String[] args) {
        System.out.println("start");
        try {
            System.out.println("try");
        } catch (ArithmeticException e) {
            System.out.println("catch");
        } finally {
            System.out.println("finally");
        }
        System.out.println("end");
    }
}
Exercise

When does the finally block execute?

Exercise

What does this print? nums has three elements, indexed 0 to 2. Predict the exact output, three lines.

public class Main {
    public static void main(String[] args) {
        int[] nums = {10, 20, 30};
        try {
            System.out.println(nums[3]);
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("bad index");
        } finally {
            System.out.println("cleanup");
        }
        System.out.println("done");
    }
}
Exercise

What does this print? Several catch clauses are listed. Predict the exact output, two lines.

public class Main {
    public static void main(String[] args) {
        try {
            int x = 10 / 0;
            System.out.println(x);
        } catch (NullPointerException e) {
            System.out.println("null");
        } catch (ArithmeticException e) {
            System.out.println("math");
        } catch (RuntimeException e) {
            System.out.println("runtime");
        }
        System.out.println("after");
    }
}
Exercise

You write catch (RuntimeException e) immediately followed by catch (ArithmeticException e). What happens?

Recap

  • An exception is an object that carries a failure; Throwable splits into Error, which you do not catch, and Exception.
  • RuntimeException and its subclasses are uncheckedArithmeticException, NullPointerException, IndexOutOfBoundsException, and friends.
  • try marks risky code; catch clauses are scanned top to bottom, and the first matching type wins.
  • finally runs in every case — success, a caught exception, or an uncaught one — so it is where cleanup belongs.
  • List specific catch types before broad ones, or the broad clause makes the specific one unreachable and the build fails.

Next you will meet the exceptions the compiler refuses to let you ignore — the checked ones — and the rule that decides what your own methods must declare.

Checkpoint quiz

An int division by zero (10 / 0) throws which exception?

A try block completes normally with no exception thrown. Which blocks run?

Go deeper — technical resources