exceptions
Throwable, Error, Exception. checked vs unchecked.
- chapter
- chapter 04.04
- reading time
- 8 min read
an exception is an object describing what went wrong. throw hands it up the call stack until some caller catches it, or it falls off the end of the thread and gets printed.
everything throwable extends Throwable, in three families: Error (the JVM is in trouble, don't catch), checked exceptions (the compiler makes callers deal with them) and unchecked ones, RuntimeException and below (usually a bug, no forcing).
the picture
someone pulls the fire alarm on floor 7 (that's throw). the alarm doesn't go to whoever's nearest: it goes down the building, floor by floor, to the people who sent you up there (the callers), and the first one whose job it is to handle fires handles it (that's catch). if nobody does, everyone's evacuated: the thread dies and the alarm gets printed on stderr.
void main() {
try {
int[] a = new int[3];
a[3] = 1; // the JVM throws for you
} catch (ArrayIndexOutOfBoundsException e) { // e is just an object
IO.println(e.getMessage());
IO.println(e.getClass().getName());
IO.println(e.getStackTrace()[0]); // where it happened
}
}Index 3 out of bounds for length 3 java.lang.ArrayIndexOutOfBoundsException Main.main(Main.java:4)
this one needs a real JVM (catching a runtime exception), so it doesn't run in the browser. copy it into java Main.java to try it.
you can throw your own just as easily:
throw new IllegalArgumentException("qty must be > 0");new builds the object, throw starts the trip down the stack. exactly how that trip goes, frame by frame, is the next chapter: try, catch, finally.
the family tree
- anything you can throw or catch
- ├── the JVM or the deployment is broken
- │ ├──
- │ │ ├── OutOfMemoryError
- │ │ └── StackOverflowError
- │ ├──
- │ │ ├── NoClassDefFoundErrorcompiled against it, missing at runtime
- │ │ └── ExceptionInInitializerError
- │ └── AssertionErrora failed assert
- └── checked, unless it's a RuntimeException
- ├──
- │ ├── FileNotFoundException
- │ ├──
- │ │ └── NoSuchFileException
- │ └──
- │ └── SocketTimeoutException
- ├── InterruptedExceptionsomeone asked this thread to stop
- ├──
- │ └── ClassNotFoundException
- ├── TimeoutException
- ├── ExecutionExceptiona Future's task threw
- └── unchecked: usually a bug
- ├── NullPointerException
- ├──
- │ └── NumberFormatException
- ├── IllegalStateException
- ├──
- │ ├── ArrayIndexOutOfBoundsException
- │ └── StringIndexOutOfBoundsException
- ├── ArithmeticException/ by zero, *Exact overflow
- ├── ClassCastException
- ├── UnsupportedOperationException
- ├── ConcurrentModificationException
- ├── NoSuchElementException
- ├── UncheckedIOExceptionan IOException, wrapped
- └── DateTimeException
RuntimeException is unchecked. the hierarchy is real, read from jdk 25 with reflection.Throwable holds everything an exception carries: a message, an optional cause (another throwable that led to this one), a stack trace, and a list of suppressed exceptions (for try-with-resources). the subclasses mostly just add a name. the name is the information: catch picks handlers by class.
checked vs unchecked
checked exceptions are part of a method's signature. if a method can throw one, it must say so with throws, and every caller must either catch it or declare it too. javac enforces this, not the JVM:
import java.nio.file.Files;
import java.nio.file.Path;
public class Config {
static String load(Path p) {
return Files.readString(p);
}
}Config.java:6: error: unreported exception IOException; must be caught or declared to be thrown
return Files.readString(p);
^two ways out: handle it with a try/catch, or pass it on by adding throws IOException to the signature, which makes it your callers' problem. unchecked exceptions (RuntimeException, Error and their subclasses) skip all of that. you can still catch them, nobody makes you.
so which should your code throw? three questions settle almost every case:
- 1.is the JVM itself in trouble?no↓
- 2.did the caller break a rule it could have checked first?yes→unchecked
- 3.could a correct program still hit this, and do something about it?·
the caller could have checked. it's a bug in their code, and the fix is to change the code, not to catch it.
the idea behind the split: a checked exception is part of the method's contract, a thing that can go wrong even in a perfect program, so the caller should be made to think about it. an unchecked exception means someone broke the contract: the fix is to change code, not to catch.
checked exceptions vs lambdas
here's where checked exceptions get annoying. Function.apply doesn't declare any exceptions, so a lambda passed to map can't throw a checked one, even if the enclosing method declares it:
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class Lambda {
static List<String> loadAll(List<Path> paths) throws java.io.IOException {
return paths.stream().map(p -> Files.readString(p)).toList();
}
}Lambda.java:7: error: unreported exception IOException; must be caught or declared to be thrown
return paths.stream().map(p -> Files.readString(p)).toList();
^the usual fix is to wrap it in the unchecked exception the JDK made for exactly this:
paths.stream().map(p -> {
try {
return Files.readString(p);
} catch (IOException e) {
throw new UncheckedIOException(e); // keeps the original as the cause
}
}).toList();or just write the for loop, which can throw anything its method declares. (this friction is a big part of why kotlin, scala and C# dropped checked exceptions altogether. java kept them, and the modern consensus is "use them sparingly".)
reading a stack trace
when nobody catches an exception, the thread prints its class, message, stack trace and causes. here's a real one: parse catches a NumberFormatException and rethrows it wrapped in an IllegalArgumentException.
Orders.java:5 is where your code handed "three" to parseInt. in most traces, the first line from your own package is where to put the breakpoint.
record Address(String city) {}
record User(String name, Address address) {}
void main() {
var u = new User("ada", null);
IO.println(u.address().city().length());
}- an exception is an object: class, message, cause, stack trace.
Error: the JVM or the build is broken. don't catch it.- checked: the compiler forces callers to catch or declare. for problems a correct program can still hit.
- unchecked (
RuntimeException): a broken contract. fix the code, don't catch it. - in a stack trace, read the headline, then find the first frame that's yours.