Java from the ground up

Exceptions: try, catch and throw

What happens when something goes wrong, how to catch it and carry on, and how to throw a problem of your own with a reason attached.

Part 11 of 12

Advanced17 min readBack to Courses

When a line cannot do its job

Some operations can fail for reasons the code cannot control: text that is not a number, a file that is not there, a slot that does not exist. Java does not return a quiet placeholder for these. It stops that line and throws an exception, which travels up until something catches it.

Uncaught, the exception ends the program and prints a stack trace: the message, then the run of calls that led to it. That trace is not decoration. Reading it from the bottom up — the last line of your code is the interesting one — is the fastest way to find out what happened.

try, catch, finally

Wrap the risky line in try and say what to do when it fails in a catch. The block after catch names the kind of problem it handles, so one try can have several catches, each dealing with a different failure, and the first matching one runs. When two kinds of failure need the same response, one catch can name both: catch (NumberFormatException | ArithmeticException e).

A finally block runs whatever happened — the failure, the success, or even a return from inside the try. It is where cleanup belongs, and it is the only part of the structure that is never skipped.

Throwing your own, and passing the problem on

throw new IllegalArgumentException("negative") is how your own code reports that a value cannot be used. The message you attach is what a reader will see, so make it say what was wrong rather than which line it came from.

Java splits exceptions in two. An unchecked one — anything extending RuntimeException — may be thrown anywhere and caught anywhere; that is the kind used here. A checked one must be declared in the method signature with throws, and the caller then has to deal with it. Both exist so that a failure nobody handled is a compile error rather than a surprise in production.

The exception hierarchy

All failures descend from Throwable. Exception is the branch for problems a program might recover from, and Error is the branch for problems it usually cannot, such as running out of memory. Under Exception, RuntimeException marks the unchecked ones.

A catch matches the type you name and every subtype below it. That is why catch order matters: a catch (Exception e) placed before catch (IllegalArgumentException e) makes the second unreachable, and the compiler rejects it.

Unwinding, and what finally guarantees

When a line throws, Java abandons the rest of that method and looks for a matching catch in the caller, then the caller of the caller, until it finds one. Each abandoned frame is removed as it goes. If no catch matches, the program ends and prints the trace.

A finally block runs on the way out of the try, whether it succeeded, threw, or returned. It is for cleanup, not for decisions: a return inside finally discards the earlier return value, which is one of the few ways to make an exception disappear.

Reading a stack trace

A stack trace is read from the top. The first line names the exception and its message; the next line is the method and file where it was thrown; the lines below are the callers, each one further from the failure. The frame at the top is where the program stopped, and the first frame that names your own class is usually where the mistake is.

Two details save time. Caused by: marks a second exception that the first one wrapped, and the lowest Caused by is the original problem. The numbers in brackets are line numbers, so the trace points at an exact line rather than at a method in general. When the trace is long, read the first three lines and the last Caused by, and ignore the middle until those two disagree.

Catching one problem, and declaring another

static int read(String text) throws Exception {
    if (text.isEmpty()) {
        throw new Exception("nothing to read");
    }
    return text.length();
}

try {
    System.out.println(read("four"));
    System.out.println(read(""));
} catch (NumberFormatException e) {
    System.out.println("not a number");
} catch (Exception e) {
    System.out.println("failed: " + e.getMessage());
} finally {
    System.out.println("attempt finished");
}

Three inputs, two failures, one loop

Press Run and follow the three attempts. Then make the method throw when the age is over 120 as well, and add an input to the array that triggers it.

Output
Click "Run" to execute your code

Three exception mistakes: catching Exception when you only know how to handle one narrow failure, swallowing the error with an empty catch block, and putting a return in finally. The first hides bugs, the second makes them silent, and the third overrides the answer the try block was about to give.

Try these next

  • Write a method that parses a whole number and throws IllegalArgumentException when it is negative; call it with a good and a bad value.
  • Add a finally that prints a line, then make the try return early and confirm the line still appears.
  • Catch NumberFormatException and IllegalArgumentException in that order; then swap them and read the compiler error.

A score reader with its own failure

Press Run and follow the three checks. Then add an input of `101` and a fourth of `-1`, and decide whether the message or the type of exception carries the useful information.

Output
Click "Run" to execute your code

Catch the narrowest kind of problem you can actually handle, and let the rest travel. A catch that swallows every failure hides bugs instead of fixing them.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What does a catch block do?