← Fundamentals
Guided lesson 6 questions · 6 languages

Errors as values or exceptions

Return codes, error values, Result, and exceptions: how a failure reaches the code that asked.

A failure reaches the caller in one of two ways: as a returned value that the caller must look at, or as a jump up the call stack that skips code until something catches it. Each language has a usual way, and each way can hide a failure in its own place.

Before you begin

Why this matters

A failure that reaches the wrong place, or no place, gives a wrong answer that looks right. Knowing how Java passes failures lets you see which lines are skipped, and which exceptions the compiler makes you handle.

What this helps you reason about

  • Predict which lines run after a throw, and which catch block receives it.
  • Tell checked from unchecked exceptions, and read the compiler error for an unreported one.
  • Spot a catch (Exception e) that hides a bug, and turn an exception into an Optional or back.

A first-aid kit holds 3 plasters, and two patients need 2 each. The second take must fail. In Java, that failure usually travels as an exception: it leaves the method at once and skips code until a catch block receives it. The compiler checks some exceptions and not others.

Already know this? The short version
  • Java failures usually travel as exceptions: throw leaves the method at once, and every frame between the throw and the nearest matching catch ends without running its remaining lines; finally blocks still run.
  • Checked exceptions (Exception but not RuntimeException) must be caught or declared with throws; unchecked ones, such as IllegalStateException and NumberFormatException, need neither, so the signature does not show them.
  • catch (Exception e) also catches bugs such as ArrayIndexOutOfBoundsException; catch the narrowest class you can handle.
  • A method can return a failure as a value instead, such as Optional.empty(); orElseThrow turns it back into an exception.

Go to the final check· Go to the practice tickets

Words in this lesson (6)
call stack
The frames of all method calls in one thread that have not finished yet, with the newest on top. A new call adds a frame on top, and a return removes it. Only the newest call runs; each call below it waits for the call it made.
exception
An object that is thrown to stop the normal flow when an operation fails, such as the NumberFormatException from Integer.parseInt. A catch block can handle it.
propagate
To pass a failure on to your own caller instead of handling it. An exception propagates by itself; a failure returned as a value propagates only when each caller returns it again.
checked exception
An exception that the compiler makes code handle: a method that can throw one must catch it or declare it with throws. Exception and its subclasses are checked, except RuntimeException and its subclasses.
unchecked exception
An exception of class RuntimeException or Error, or one of their subclasses, such as IllegalStateException. The compiler does not make code catch it or declare it.
Optional
A class from java.util whose object holds one value or none. Optional.ofNullable(x) holds x, or nothing when x is null, and orElse(d) gives the value, or d when there is none.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / Two ways

How can a failure reach the caller?

A function that cannot give its result must tell its caller. Languages use two ways.

In the first way, the failure is a value. The function returns in the normal way, and what it returns says that it failed. The caller must look at that value to notice the failure.

In the second way, the failure is an exception. The function does not return in the normal way. The failure jumps up the call stack: the frame of the failing function ends, then its caller’s frame ends, and so on, until the failure reaches code that catches it. The lines in between are skipped.

Java uses exceptions. throw starts the jump. try { … } catch (IllegalStateException problem) { … } runs the code in try, and runs the catch block only if an exception of that class, or a subclass, was thrown there; problem names it. getMessage() gives the text the exception was made with.

Main.java
static int take(int left, int wanted) {
    if (wanted > left) {
        throw new IllegalStateException(
                "need " + wanted + ", have " + left);
    }
    return left - wanted;
}

try {
    System.out.println(take(3, 4));
} catch (IllegalStateException problem) {
    // prints failed: need 4, have 3
    System.out.println("failed: " + problem.getMessage());
}

A failure reaches the caller either as a value it must look at, or as an exception that jumps up the call stack. Java uses exceptions.

Coming from Go: A failed Java call does not return a value at all. The code after it in the same try is skipped, and there is no err to test.

Experiment

Follow a throw up the call stack

throw ends take at once. treat has no catch of its own, so its frame ends too, before its println line. The nearest matching catch is in main, and the program continues there with the thrown exception.

Why it matters An exception skips every line between the throw and the catch, in every frame it leaves.

Predict which lines run after the second take fails, and what is printed. Then change treat(3) to treat(4) and predict again.

class Main {
    static int take(int left, int wanted) {
        if (wanted > left) {
            throw new IllegalStateException(
                    "need " + wanted + ", have " + left);
        }
        return left - wanted;
    }

    static int treat(int left) {
        int afterFirst = take(left, 2);
        int afterSecond = take(afterFirst, 2);
        System.out.println("both patients treated");
        return afterSecond;
    }

    public static void main(String[] args) {
        try {
            System.out.println("left: " + treat(3));
        } catch (IllegalStateException problem) {
            System.out.println("failed: " + problem.getMessage());
        }
    }
}
What to look for

Step into treat and into each take. The frame list shows Main.take on top of Main.treat, on top of Main.main. In the second take, left is 1 and the stepper stops on the throw. The next stop is the catch line in main: the take and treat frames are gone together, and the println line in treat never ran. At the catch, problem shows java.lang.IllegalStateException@ and an id, with the exception’s internal fields. The run prints failed: need 2, have 1. After the edit, both takes succeed, treat prints both patients treated, and the run prints left: 0.

Expected output

failed: need 2, have 1

02 / Values

What does a failure look like as a value?

Java can also give back a failure as a value. A method that returns Optional<Integer> says in its signature that there may be no result, as in Absence, zero, and empty values. Optional.empty() holds nothing, Optional.of(x) holds x, and isPresent() tells them apart. An Optional carries no reason; to carry one, return a small record that holds either a result or a reason, as in Alternatives and combinations.

Main.java
static Optional<Integer> tryTake(int left, int wanted) {
    if (wanted > left) {
        return Optional.empty();
    }
    return Optional.of(left - wanted);
}

Optional<Integer> rest = tryTake(3, 4);
if (rest.isPresent()) {
    System.out.println(rest.get());
} else {
    System.out.println("kit short");
}

A failure returned as a value shows in the signature, so every caller sees it.

03 / Exceptions

What happens when a failure jumps up the call stack?

Java has two kinds of exception. A checked exception, such as Exception itself or IOException, must be handled: a method that can throw one must catch it, or declare it with throws in its signature. Without throws Exception below, javac reports error: unreported exception Exception; must be caught or declared to be thrown.

An unchecked exception is a RuntimeException or one of its subclasses, such as IllegalStateException or NumberFormatException. The compiler asks nothing about it. A finally block after try runs on both paths, after a normal end or while an exception passes. A later lesson on Java checked and unchecked exceptions goes further.

Main.java
static int take(int left, int wanted) throws Exception {
    if (wanted > left) {
        throw new Exception("need " + wanted + ", have " + left);
    }
    return left - wanted;
}

Checked exceptions appear in the signature and must be handled; unchecked ones do not.

Precisely: a stack trace

An exception records the stack trace when it is created. An exception that nobody catches stops the thread, and Java prints its class, its message, and the stack trace, newest frame first.

04 / Costs

Where can each way hide a failure?

Each way has a cost. A failure returned as a value, such as an Optional, shows in the signature, but a caller can still call get() without testing. An exception cannot be dropped without a word: if nobody catches it, the program stops with a message. But an unchecked exception does not show in the signature; the caller learns about it from the documentation, or from a crash.

The common Java trap is catch (Exception problem). It catches every exception class below Exception: the failure you expected, and also an ArrayIndexOutOfBoundsException from a bug. One fixed message for all of them turns a bug into a wrong message. Catch the narrowest class you can handle, and let the others pass.

Main.java
try {
    int rest = take(kits[0], 2);
    System.out.println(rest);
} catch (IllegalStateException problem) {
    System.out.println("not enough plasters");
}

A value shows in the signature but can be ignored; an exception cannot be dropped silently, but an unchecked one does not show in the signature, and catch (Exception e) hides bugs.

Coming from Python: catch (Exception e) is Java’s except Exception: it also catches mistakes in your own code.

Experiment

Catch more than you meant to

kits[1] throws ArrayIndexOutOfBoundsException before take is called. catch (Exception problem) catches every exception class below Exception, so it takes this one too, and prints a message written for a different failure.

Why it matters Catch the narrowest class you can handle, here IllegalStateException. A catch (Exception e) turns a bug into a wrong message.

Predict what is printed: is the kit short of plasters? Then change the line in the catch block to System.out.println("failed: " + problem); and predict again.

class Main {
    static int take(int left, int wanted) {
        if (wanted > left) {
            throw new IllegalStateException(
                    "need " + wanted + ", have " + left);
        }
        return left - wanted;
    }

    public static void main(String[] args) {
        int[] kits = {5};
        try {
            int rest = take(kits[1], 2);
            System.out.println("treated; " + rest + " plasters left");
        } catch (Exception problem) {
            System.out.println("not enough plasters");
        }
    }
}
What to look for

Step from the line int rest = take(kits[1], 2);. The next stop is the catch line, and no take frame ever appears: take never ran. kits shows int[]@ and an id, then [5]: one element, at index 0. At the catch, problem shows java.lang.ArrayIndexOutOfBoundsException@ and an id. The run prints not enough plasters. After the edit, the run prints failed: java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1.

Expected output

not enough plasters

05 / Converting

How do you turn one kind of failure into the other?

You can turn one kind into the other wherever the caller needs it. To turn an exception into a value, catch it close to the call and return Optional.empty(). To turn an Optional back into an exception, call orElseThrow(), which throws NoSuchElementException when there is no value.

A checked exception can also become an unchecked one: catch it and throw new IllegalStateException("…", problem). The second argument keeps the original exception as the cause, so its message and stack trace are not lost.

Main.java
static Optional<Integer> takeOrEmpty(int left, int wanted) {
    try {
        return Optional.of(take(left, wanted));
    } catch (IllegalStateException problem) {
        return Optional.empty(); // the reason is dropped here
    }
}

int rest = takeOrEmpty(5, 4).orElseThrow();

Catch and return a value where the caller can act; throw where it cannot, and keep the cause.

06 / Try it

Can you explain a fresh example?

Predict every line of output, then compare with the answer. The next lesson, Stack and heap, shows where values are stored while a program runs. Its link is at the end of the page.

Main.java
class Main {
    static int take(int left, int wanted) {
        if (wanted > left) {
            throw new IllegalStateException(
                    "need " + wanted + ", have " + left);
        }
        return left - wanted;
    }

    static int treat(int left) {
        System.out.println("start");
        int rest = take(left, 4);
        System.out.println("done");
        return rest;
    }

    public static void main(String[] args) {
        try {
            System.out.println(treat(3));
        } catch (IllegalStateException problem) {
            System.out.println("caught");
        } finally {
            System.out.println("end");
        }
    }
}

What is printed, line by line?

Reveal the reasoning

start, caught, end. treat prints start and calls take, which throws. The exception ends take and treat at once, so done is never printed and treat never returns a value to print. The catch block prints caught, and the finally block then prints end; it would also have run if nothing had been thrown.

Practice tickets

  1. ERR-101 · An unreadable weight gives a dose of 0 mg → Free
  2. ERR-102 · One unreadable drink hides the rest of the fluid chart → Sign in · Pro
  3. ERR-103 · Medicine packs vanish when a ward cabinet is full → Sign in · Pro

Keep exploring

Stack and heap →

Primary references