skip to content
everything.coreApi.exceptions

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.

Main.java
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
    }
}
output, from jdk 25
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:

java
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
click a class to fold it. Error means the JVM or the deployment is broken. amber classes are checked: the compiler won't let you ignore them. everything under 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:

Config.java
import java.nio.file.Files;
import java.nio.file.Path;
 
public class Config {
    static String load(Path p) {
        return Files.readString(p);
    }
}
text
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. 1.is the JVM itself in trouble?no↓
  2. 2.did the caller break a rule it could have checked first?yes→unchecked
  3. 3.could a correct program still hit this, and do something about it?·
uncheckedextends RuntimeException
throw new IllegalArgumentException("qty must be > 0: " + qty);

the caller could have checked. it's a bug in their code, and the fix is to change the code, not to catch it.

pick a situation and follow the questions. the answer is the kind of throwable, and the line below is what the JDK itself throws in that situation.

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.

quick check
which one will the compiler make you catch or declare?

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:

Lambda.java
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();
    }
}
text
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:

java
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.

stderr · click a line
the first frame that's yours
click any line. traces read top-down from the newest frame. the move that saves time: skip the JDK frames and find the first frame from your own code.
predict the output
java
record Address(String city) {}
record User(String name, Address address) {}
 
void main() {
    var u = new User("ada", null);
    IO.println(u.address().city().length());
}
keylines
  • 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.