Exceptions

Throwable
├── Error                    ← JVM problems, do not catch
│   ├── OutOfMemoryError
│   └── StackOverflowError
└── Exception
    ├── RuntimeException     ← unchecked
    │   ├── NullPointerException
    │   ├── IllegalArgumentException
    │   ├── IllegalStateException
    │   └── NumberFormatException
    └── IOException          ← checked

Checked exceptions describe expected trouble from the outside world; the caller must handle or declare them. Unchecked ones usually mean a bug.

try {
    var text = Files.readString(path);
} catch (IOException e) {
    IO.println("cannot read: " + e.getMessage());
}

String read(Path p) throws IOException {   // or pass it up
    return Files.readString(p);
}

try / catch / finally

try {
    risky();
} catch (NumberFormatException e) {   // specific first
    
} catch (RuntimeException e) {        // general later
    
} finally {
    // always runs
}
e.getMessage()        e.getCause()
e.getStackTrace()     e.printStackTrace()   // debugging only

try-with-resources

Anything AutoCloseable is closed for you, in reverse order, even on failure:

try (var in  = Files.newBufferedReader(source);
     var out = Files.newBufferedWriter(target)) {

    String line;
    while ((line = in.readLine()) != null) {
        out.write(line.toUpperCase());
        out.newLine();
    }
}

Your own exceptions

public class OverdraftException extends RuntimeException {

    private final long missing;

    public OverdraftException(long missing) {
        super("short by %d cents".formatted(missing));
        this.missing = missing;
    }

    public long missing() { return missing; }
}

Always pass the cause along — otherwise you lose the information you need later:

catch (IOException e) {
    throw new SupplierUnavailableException("lookup failed", e);
}

Preconditions

Objects.requireNonNull(from, "from is required");
if (amount <= 0) throw new IllegalArgumentException("amount must be positive: " + amount);
Situation Exception
invalid argument IllegalArgumentException
wrong object state IllegalStateException
argument was null NullPointerException via requireNonNull
not implemented yet UnsupportedOperationException
business rule broken your own type

Common mistakes

// swallowing
try { risky(); } catch (Exception e) { }        // never

// control flow
try { while (true) IO.println(list.get(i++)); }
catch (IndexOutOfBoundsException e) { }         // use a for-each loop

// useless messages
throw new IllegalArgumentException("error");    // say what and which value

Catch where you can actually react — usually high up, not in every helper method.

Cleanup without catching:

try {
    process();
} finally {
    releaseLock();     // exception still propagates
}

★ Exercises

  1. int divide(int a, int b) throwing a helpful ArithmeticException when b == 0.
  2. Write InvalidInputException and use it in a validator.
  3. A method returning an empty list instead of throwing when a file is missing — when is that good, when dangerous?
  4. <T> T withRetry(Supplier<T> task, int attempts) — retry on exception, rethrow the last one.
  5. Create a wrapped exception (cause + wrapper) and print both messages.
  6. What does a method return when try has return 1 and finally has return 2? Try it — then never write it.