Checked vs unchecked & the exception hierarchy
Throwable, Error, Exception, RuntimeException — and what the compiler forces you to handle.
The Throwable family tree
Everything you can throw or catch is a subclass of Throwable. Two branches descend from it:
- Error — serious problems the JVM raises that an application normally should *not* catch (OutOfMemoryError, StackOverflowError). You don't handle these; you let the program die.
- Exception — the conditions application code is expected to anticipate. Under it sits a special subclass, RuntimeException, which gets its own rule.
So the hierarchy is:
Throwable
├── Error (don't catch: JVM-level, unchecked)
└── Exception (checked — must declare or handle)
└── RuntimeException (unchecked — programming bugs)
Checked vs unchecked — the one distinction that matters
The compiler divides exceptions into two groups:
- Checked exceptions — Exception and its subclasses *except* RuntimeException. The compiler forces you to either catch them or declare them with a throws clause. Examples: IOException, ClassNotFoundException, and the project's own ControllerException. The reasoning: these are *recoverable external conditions* (a file is missing, a window is stuck) that a caller could reasonably plan for, so the language makes the obligation visible.
- Unchecked exceptions — RuntimeException and its subclasses (and Error). The compiler does not force handling. Examples: NullPointerException, ArrayIndexOutOfBoundsException, IllegalArgumentException, ArithmeticException. The reasoning: these almost always signal a *bug* you should fix, not a condition you should catch.
The quick test: is it a RuntimeException (or Error)? Then it's unchecked. Otherwise it's checked.
The throws clause
A method that can throw a *checked* exception and does not catch it must announce that in its signature with throws:
void loadEvents(String file) throws IOException {
// ... code that may throw IOException, not caught here ...
}
Now loadEvents's callers face the same obligation: catch the IOException, or add throws IOException to *their* signature and pass the responsibility upward. A method may list several: throws IOException, ControllerException. You never need to declare unchecked exceptions — void f() can throw NullPointerException with no throws clause.
Worked example — a custom checked exception
The Greenhouse project defines its fault type as a checked exception by extending Exception directly:
public class ControllerException extends Exception {
public ControllerException() { super(); }
public ControllerException(String why) { super(why); }
}
Because it extends Exception (not RuntimeException), any method that throws it must declare it:
void action() throws ControllerException {
if (powerOut) {
throw new ControllerException("PowerOut");
}
}
If you had instead written class ControllerException extends RuntimeException, the throws clause would become optional and the compiler would stop reminding callers to handle the fault — exactly the wrong choice for a recoverable hardware condition the controller is built to respond to. Choosing extends Exception is a deliberate design decision, and a classic exam question.