Memra

Exceptions: try, catch, throw

How a thrown exception unwinds the stack until a matching catch handles it.

Why exceptions exist

An old C-style way to report failure is to return a magic value — -1, null, a global error code — and *hope* the caller checks it. The hope usually fails: error checks get skipped, and a small problem becomes a corrupted program. Java's answer is exceptions: a separate, mandatory channel for reporting that *this method cannot complete its job*. When something goes wrong you throw an exception object; normal execution stops at that point and Java looks for code that wants to catch it.

The key idea is separation of the happy path from the error path. The try block holds the code that *should* work; the catch block holds the recovery code for when it doesn't. You no longer interleave if (errorCode != 0) after every single call.

Throwing an exception

You throw with the throw keyword and an exception *object* — exceptions are ordinary objects, instances of Throwable or one of its subclasses. The object carries information about what failed (at minimum, a message):

if (level < 0) {
    throw new IllegalArgumentException("level must be >= 0, was " + level);
}

The moment that throw runs, the method stops. Control does not return to the caller normally — instead Java begins stack unwinding: it pops the current method off the call stack, then the caller, then *its* caller, and so on, at each level asking "is there a try here whose catch can handle this exception type?" The first matching catch wins; everything between the throw and that catch is abandoned.

Catching it

A try block wraps code that might throw; one or more catch clauses follow, each naming an exception type and a handler variable:

try {
    controller.run();          // might throw ControllerException
} catch (ControllerException e) {
    System.out.println("Recovering: " + e.getMessage());
}

If run() throws a ControllerException, control jumps straight to the matching catch, binds the thrown object to e, and runs the handler. If run() completes normally, the catch is skipped entirely. The handler variable e lets you interrogate the exception — e.getMessage() returns the message string you passed to the constructor, and e.printStackTrace() dumps the call chain that led to the throw.

Worked example — the Greenhouse ControllerException

The capstone Greenhouse project throws a custom ControllerException when a hardware fault is detected, and the top level catches it to begin an emergency shutdown. Stripped to its essence:

class ControllerException extends Exception {
    public ControllerException(String why) { super(why); }
}

void checkWindow(boolean stuck) throws ControllerException {
    if (stuck) {
        throw new ControllerException("WindowMalfunction");
    }
}

The caller wraps the risky call and recovers:

try {
    checkWindow(true);
    System.out.println("window ok");   // skipped: throw happened above
} catch (ControllerException e) {
    System.out.println("fault: " + e.getMessage());  // prints: fault: WindowMalfunction
}
System.out.println("after handler");                  // always runs

Because checkWindow(true) throws, the System.out.println("window ok") line is *never reached* — control leaves the try at the throw and lands in the catch. After the handler finishes, execution continues normally with after handler. Trace this carefully: exam trace questions live exactly here — they hand you a try/catch and ask which lines print and in what order.

NORMAL ~/memra/learn/comp-308/exception-concepts-try-catch-throw utf-8 LF