Command Palette

Search for a command to run...

PHASE 7Intermediate ~31 min· topic 1 of 8

Topic 7.1

What Exceptions Are

In one line

An exception is an object that describes something that went wrong. When it is thrown, Java stops the current code, walks back up the chain of method calls looking for a matching catch, and if none is found the thread dies and prints a stack trace.

Think of it like this

A fire alarm in a school. When a child in Room 12 smells smoke, they don't keep doing maths. They pull the alarm. The teacher can't fix a fire, so the message goes up to the head teacher, and if she can't handle it, to the fire brigade. Whoever can deal with it does; everyone in between stops what they were doing. An exception is that alarm: the code that finds the problem throws it, and it travels up until someone who knows what to do catches it.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Exception
An object that describes something that went wrong while the program was running, such as dividing by zero or a missing file.
Throw
To signal a problem by handing an exception object to the JVM. Normal execution stops at that point.
Catch
To receive a thrown exception in a catch block and deal with it, so the program can carry on.
Call stack
The pile of methods that are currently running, each waiting for the one above it to finish. main is at the bottom.
Stack frame
One entry on the call stack: the local variables and the current position of one running method.
Propagation (unwinding)
An uncaught exception leaving a method and moving to its caller, frame by frame, until something catches it.
Stack trace
The printed list of methods (with file names and line numbers) that were active when the exception was created. It shows where the problem happened.
`Throwable`
The parent class of everything that can be thrown. Its two children are Error and Exception.
Error code
The older way to report failure: return a special value like -1 and hope every caller checks it.

Step by step

01A program without a safety net

Here main calls average, which calls divide. Integer division by zero is impossible, so the JVM creates an ArithmeticException with the message / by zero and throws it from inside divide.

Nobody catches it. divide stops, then average stops, then main stops. The JVM prints the exception and the stack trace, and the program exits with status code 1. Nothing after the failing line runs.

Main.javawhole filejava
public class Main {
    static int divide(int a, int b) {
        return a / b;
    }

    static int average(int total, int count) {
        return divide(total, count);
    }

    public static void main(String[] args) {
        System.out.println(average(90, 0));
    }
}
terminal
$ java Main.java
── expected output ──
Exception in thread "main" java.lang.ArithmeticException: / by zero
at Main.divide(Main.java:3)
at Main.average(Main.java:7)
at Main.main(Main.java:11)

02Reading a stack trace

Read a stack trace top down. The first line is the exception's class and message: what went wrong. The first at line is where the exception was created: divide, line 3. Each following line is the caller that was waiting: average at line 7, then main at line 11.

In real applications the trace is long, and most frames belong to frameworks. Look for the first frame that is your code; that's usually where the bug is. If the trace has a Caused by: section (Topic 7.5), the real root cause is the last Caused by:.

Reading a stack tracediagram
Rendering diagram…

03Catching it: the program survives

Wrap the risky call in try { ... } catch (ArithmeticException e) { ... }. When the exception propagates out of average, the JVM finds this matching catch in main, jumps there, and gives the handler the exception object in the variable e.

After the catch block finishes, execution continues with the next statement after the whole try statement. The frames of divide and average are gone: you can't "go back" into them. Topic 7.2 covers try, catch and finally in detail.

Main.javawhole filejava
try {
    System.out.println(average(90, 0));
} catch (ArithmeticException e) {
    System.out.println("could not average: " + e.getMessage());   // / by zero
}
System.out.println("still running");

04An exception is just an object

new IllegalStateException("not started") creates an object exactly like new StringBuilder() does. Nothing happens until you throw it. You can store it in a variable, pass it to a method, put it in a list, or print it.

The useful methods come from Throwable: getMessage() (the text you passed, or null), toString() (class name: message), getCause(), getStackTrace() (an array of StackTraceElement), and printStackTrace() (writes the full trace to System.err).

The stack trace is captured when the object is created, not when it is thrown. Creating an exception in one method and throwing it from another shows the creation site, a detail that occasionally confuses people reading logs.

Main.javawhole filejava
Throwable t = new IllegalStateException("not started");   // nothing thrown yet
System.out.println(t.getMessage());   // not started
System.out.println(t);                // java.lang.IllegalStateException: not started
throw (IllegalStateException) t;      // now it is thrown

05The family tree

All throwable types form one class hierarchy, so a catch for a parent type also catches every child type. Catching Exception catches IOException and all RuntimeExceptions, but not Errors.

The split between Error, checked exceptions and unchecked (RuntimeException) exceptions is the most important map in this phase. You'll come back to it in Topic 7.3.

The family treediagram
Rendering diagram…

06Under the hood: the exception table

A try block adds no instructions to the happy path. Instead, javac writes an exception table next to the method's bytecode. Each row says: if an exception of this type happens between instruction from and instruction to, jump to target.

When something is thrown, the JVM checks the current method's table for a row covering the current instruction with a matching type. If one matches, it clears the operand stack, pushes the exception, and jumps. If not, it pops the frame and repeats in the caller. That's stack unwinding, done by the JVM, not by your code.

Safe.javawhole filejava
static int safeDivide(int a, int b) {
    try {
        return a / b;
    } catch (ArithmeticException e) {
        return 0;
    }
}
terminal
$ javac Safe.java
javap -c Safe
── expected output ──
static int safeDivide(int, int);
Code:
0: iload_0
1: iload_1
2: idiv
3: ireturn
4: astore_2
5: iconst_0
6: ireturn
Exception table:
from to target type
0 3 4 Class java/lang/ArithmeticException

07Exceptions vs error codes

With error codes, int r = parse(text); if (r == -1) ... the check is optional, and -1 might also be a valid answer. Every caller in the chain must check and pass the code upward by hand.

With exceptions, the code that detects the problem throws, the code that can handle it catches, and everything in between stays clean. The price: control can leave any method at any point where something can be thrown, so you must write code that stays correct when interrupted. That's what finally and try-with-resources (Topics 7.2 and 7.6) are for.

Try it yourself

  1. 1

    Predict the skipped lines

    In the first example, change average(90, 3) to average(90, 0) too (outside the try). Before running, write down every line you expect to see and what happens to main carries on. Then run it. The program now ends with a stack trace instead of the last line.

  2. 2

    Add a level to the stack

    In the stack-trace example, add level0() that calls level1(), and call level0() from main. Predict the new list of frames, then run.

  3. 3

    Find the parent classes

    In the hierarchy example, add new NumberFormatException("x") and new java.io.FileNotFoundException("x") to the array. Predict the three true/false columns for each, then run. Then print getSuperclass() for both.

Code & diagrams

Watch an exception travel up the stack New tab

With a zero count, the 'finished' lines never print: each frame is abandoned as the exception passes through it.

Sign in to run this example in your browser.

Expected output

main starts
 average starts
  divide(90, 3) starts
  divide finished
 average finished
result: 30
 average starts
  divide(90, 0) starts
caught in main: java.lang.ArithmeticException: / by zero
main carries on
Exceptions are objects in a class hierarchy New tab
Sign in to run this example in your browser.

Expected output

ArithmeticException | message=/ by zero | Exception? true | Runtime? true | Error? false
IOException | message=disk not ready | Exception? true | Runtime? false | Error? false
StackOverflowError | message=null | Exception? false | Runtime? false | Error? true
IllegalArgumentException | message=age must be >= 0 | Exception? true | Runtime? true | Error? false
java.lang.IllegalStateException: not started
parent class: java.lang.RuntimeException
Read the stack trace from code New tab

Line numbers are left out on purpose so the output is stable. getLineNumber() gives them when you need them.

Sign in to run this example in your browser.

Expected output

message: deep problem
  at Main.level3
  at Main.level2
  at Main.level1
  at Main.main
Error codes vs exceptions (fragment)java
// Error-code style: easy to forget the check, and -1 might be a real value
static int parseAgeOrMinusOne(String text) {
    for (char c : text.toCharArray()) {
        if (!Character.isDigit(c)) return -1;
    }
    return Integer.parseInt(text);
}

// Exception style: the failure can't be silently ignored
static int parseAge(String text) {
    int age = Integer.parseInt(text);          // throws NumberFormatException for "abc"
    if (age > 150) {
        throw new IllegalArgumentException("age too large: " + age);
    }
    return age;
}

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Index past the end of an array

In main, write int[] marks = {90, 80}; System.out.println(marks[2]); with no try/catch.

terminal
$ java Main.java
── what you'll see ──
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 2 out of bounds for length 2
at Main.main(Main.java:4)

Break #2

Recurse forever

Write static int countDown(int n) { return countDown(n - 1); } (no base case) and call it from main.

terminal
$ java Main.java
── what you'll see ──
Exception in thread "main" java.lang.StackOverflowError
at Main.countDown(Main.java:2)
at Main.countDown(Main.java:2)
at Main.countDown(Main.java:2)
...

Myth vs fact

Myth

An exception means the program crashes.

Fact

Only an uncaught exception ends the thread. Caught exceptions are a normal, controlled way to handle failure, and well-written servers catch and handle them all day without stopping.

Myth

try blocks slow the code down.

Fact

Entering a try block costs nothing: javac only records an exception table. The cost is in creating the exception (capturing the stack trace) and unwinding, which only happens when something is actually thrown.

Myth

Errors and exceptions are the same thing.

Fact

Error and Exception are sibling classes under Throwable. Errors like OutOfMemoryError mean the JVM itself is in trouble; catch (Exception e) does not catch them, and application code should rarely try.

Myth

The stack trace shows where the exception was thrown.

Fact

It shows where the exception object was created (fillInStackTrace runs in the constructor). Usually that's the same line, but not when an exception is created in one place and thrown elsewhere.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    The JVM can throw some exceptions without allocating a fresh object or trace. With -XX:+OmitStackTraceInFastThrow (on by default in HotSpot), a JIT-compiled hot path that throws the same implicit NullPointerException, ArithmeticException or ArrayIndexOutOfBoundsException many times may switch to a preallocated exception with no message and no stack trace. If your logs suddenly show java.lang.NullPointerException with no trace, look further back for the first occurrence, or run with -XX:-OmitStackTraceInFastThrow.

  • ▸

    Throwable has a protected constructor Throwable(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) (Java 7). Passing writableStackTrace = false skips fillInStackTrace() and makes creation cheap. Some high-performance libraries use stackless exceptions for control flow. Overriding fillInStackTrace() to return this was the older trick.

  • ▸

    Stack unwinding is driven by the exception table: each row's range is half-open (from inclusive, to exclusive), rows are searched in order, and the type is matched with instanceof semantics. A finally block appears as extra rows with type any (Topic 7.2).

  • ▸

    StackWalker (Java 9) lets you inspect the current stack lazily without creating an exception, which is the right tool for logging the caller's name. Thread.setDefaultUncaughtExceptionHandler replaces the JVM's default 'print and die' behaviour for threads that don't catch.

Remember this

  1. 1

    An exception is an ordinary Java object. It's an instance of a class that extends java.lang.Throwable, created with new on the heap like any other object. It carries a message (getMessage()), an optional cause (another exception that led to this one), and a stack trace: the list of method calls that were active when it was created.

  2. 2

    Throwing an exception (throw new IllegalArgumentException("age < 0"), or the JVM doing it for you on 10 / 0) stops normal execution immediately. The rest of the current statement and method is skipped. The JVM looks for a catch block that matches in the current method; if there isn't one, the method's stack frame is thrown away and the search continues in the caller. This walk up the call stack is called propagation or stack unwinding.

  3. 3

    If the exception reaches the top of a thread's stack without being caught, the thread ends. For the main thread, the JVM's default handler prints Exception in thread "main", the exception's class and message, and the stack trace, then the program exits with status 1. Other threads keep running if they're not daemon threads (that's a concurrency subject for later).

  4. 4

    The hierarchy matters. Throwable has two branches. **Error** is for serious problems in the JVM itself, such as OutOfMemoryError and StackOverflowError; ordinary code shouldn't try to recover from them. **Exception** is for problems an application might handle. Under Exception, **RuntimeException** and its subclasses (NullPointerException, IllegalArgumentException, ArithmeticException, IndexOutOfBoundsException...) are unchecked; everything else under Exception (IOException, SQLException...) is checked, which Topic 7.3 explains.

  5. 5

    Exceptions replace error codes. In C, a function returns -1 and every caller must remember to check it. Forget once and the program carries on with garbage. A Java exception can't be silently ignored by accident: either some code catches it, or the thread stops with a clear report of what failed and where. It also separates the happy path from the error handling, so the main logic reads top to bottom.

  6. 6

    Exceptions aren't free. Creating one calls fillInStackTrace(), which walks the current stack and records every frame, and that's the expensive part (throwing and catching within one method is comparatively cheap). That's why exceptions are for exceptional situations, not for normal control flow like ending a loop. A try block itself costs nothing at run time when no exception is thrown: the compiler records the protected range in an exception table instead of adding instructions.

Explain it without notes

01

What is an exception in Java, and what happens step by step when one is thrown?

02

What is the difference between Error, checked exceptions and unchecked exceptions?

03

How do you read a stack trace to find the bug?

04

Why are exceptions better than returning error codes, and what do they cost?

Practice

01

Write a program that calls Integer.parseInt("12a") inside a try block and prints the exception's simple class name and message.

02

Write three methods a(), b(), c() where c() throws new UnsupportedOperationException("not yet"). Catch it in main and print how many frames the stack trace has and the name of the top method.

03

Build an array of three Throwables (new OutOfMemoryError(), new java.io.IOException(), new NullPointerException()) and, for each, print whether a catch (Exception e) block would catch it.

Trade-offs

  • ↔

    Exceptions vs return values: exceptions are right for failures the immediate caller usually can't handle; for an expected 'not found' outcome, returning Optional or a result type (Phase 10) is cheaper and makes the outcome part of the signature.

  • ↔

    Fail fast vs carry on: letting a bug's exception propagate stops the work but gives a clear trace; catching it too early and continuing hides the bug and corrupts data later. Catch only where you can actually do something useful.

  • ↔

    Rich traces vs speed: stack traces are what make exceptions debuggable, but capturing them costs time. Keep them for real failures, and don't throw exceptions in tight loops as a substitute for an if.

Done when you can

  • Done when you can explain what happens on the call stack when an exception is thrown and not caught.

  • Done when you can read a stack trace and point to the line that failed and the chain of callers.

  • Done when you can draw the Throwable / Error / Exception / RuntimeException hierarchy from memory.

  • Done when you can use getMessage(), toString() and getStackTrace() on a caught exception.

  • Done when you can say why a try block is free but throwing an exception is not.