# Exception Handling

## 1. The `Throwable` Hierarchy

Everything that can be thrown extends `Throwable`. Java splits it into two conceptually different branches, and further splits `Exception` into checked and unchecked.

```
Throwable
  ├── Error                    — serious, not meant to be caught (OutOfMemoryError, StackOverflowError)
  └── Exception
        ├── RuntimeException    — UNCHECKED (NullPointerException, IllegalArgumentException, ...)
        └── (everything else)   — CHECKED (IOException, SQLException, ...)
```

```java
try {
    riskyIO();
} catch (java.io.IOException e) {
    // handle it
}
```

## 2. Checked vs Unchecked — a Compile-Time Distinction

**Checked** exceptions must be either caught or declared with `throws` in the method signature — the compiler enforces this. **Unchecked** exceptions (`RuntimeException` and its subclasses, plus all `Error`s) require no such declaration; they can propagate silently through any number of method signatures with no compiler involvement.

```java
void readFile() throws java.io.IOException { // MUST declare — IOException is checked
    java.nio.file.Files.readString(java.nio.file.Path.of("data.txt"));
}

void divide(int a, int b) {
    if (b == 0) throw new ArithmeticException("/ by zero"); // unchecked — no `throws` needed
}
```

This is a real, intentional Java design decision with no equivalent in JavaScript/TypeScript (where *every* thrown value is effectively "unchecked" from the type system's point of view) — checked exceptions are meant for conditions a well-behaved caller could reasonably anticipate and recover from (a missing file), while unchecked exceptions signal programming errors (a null reference, an illegal argument) that indicate a bug rather than an expected condition.

## 3. `try` / `catch` / `finally`

```java
try {
    doSomething();
} catch (java.io.FileNotFoundException e) {
    System.out.println("specific: " + e.getMessage());
} catch (java.io.IOException e) {
    System.out.println("more general IO problem: " + e.getMessage());
} finally {
    System.out.println("always runs — cleanup goes here");
}
```

Catch clauses are checked top to bottom, and the first assignable match wins — so subclasses must be listed **before** their superclasses, or the more general catch shadows the specific one and the compiler rejects unreachable catch blocks entirely. `finally` runs whether the try block succeeds, throws, or even executes a `return` — it is the one block Java guarantees will execute (barring `System.exit()` or a JVM crash).

## 4. Multi-Catch

When multiple exception types need identical handling, combine them with `|` instead of duplicating the catch body:

```java
try {
    process();
} catch (java.io.IOException | InterruptedException e) {
    System.out.println("failed: " + e.getMessage());
}
```

The caught variable's static type is the least upper bound of the listed types, and (unlike separate catch blocks) it is implicitly `final` — you cannot reassign it.

## 5. try-with-resources

Any object implementing `AutoCloseable` can be declared in the `try(...)` parentheses; its `close()` is called automatically when the block exits, in reverse declaration order, even if an exception was thrown.

```java
try (var reader = new java.io.BufferedReader(new java.io.FileReader("data.txt"))) {
    System.out.println(reader.readLine());
} // reader.close() called automatically here, no finally needed
```

## 6. Throwing Custom Exceptions and Exception Chaining

```java
class InsufficientFundsException extends Exception {
    InsufficientFundsException(String message, Throwable cause) {
        super(message, cause); // preserves the ORIGINAL exception as the "cause"
    }
}

void withdraw(double amount) throws InsufficientFundsException {
    try {
        // ... some lower-level operation that might throw ...
    } catch (java.sql.SQLException e) {
        throw new InsufficientFundsException("withdrawal failed", e); // chain, don't swallow
    }
}
```

`getCause()` retrieves the original exception; printed stack traces show `Caused by:` chains, which is essential for debugging — swallowing the original exception loses exactly the information you need most.

## 7. Custom Exception Design: Checked or Unchecked?

```java
class ValidationException extends RuntimeException {  // unchecked — a caller bug (bad input)
    ValidationException(String message) { super(message); }
}

class ConfigFileMissingException extends Exception {    // checked — an expected, recoverable condition
    ConfigFileMissingException(String message) { super(message); }
}
```

Extend `RuntimeException` for programmer errors and invariant violations; extend `Exception` directly only when you genuinely want to force callers to handle or explicitly propagate the condition.

![Each method's bytecode carries an exception table of (start, end, handler, type); when an exception is thrown, the JVM walks up the call stack frame by frame looking for a matching range](diagrams/01-exception-table.svg)

## Under the Hood

**There's no `setjmp`/`longjmp`-style control transfer — the JVM uses a per-method exception table.** Every compiled method that contains a `try` block gets a compact table in its class file: rows of `(start_pc, end_pc, handler_pc, catch_type)` describing which bytecode-offset ranges are protected by which handler, for which exception type. When a `throw` (bytecode `athrow`) executes, the JVM does not search source code or unwind via a signal mechanism — it consults the *current* method's exception table for a range covering the current program counter with a matching (or superclass-matching) `catch_type`. If none matches, the JVM pops the current stack frame entirely (this is "stack unwinding") and repeats the same table lookup in the *caller's* frame, continuing outward until either a handler matches or the stack is exhausted, at which point the thread terminates (or, for the main thread, the exception is printed and the JVM exits).

**The expensive part of throwing is capturing the stack trace, not the throw/catch mechanism itself.** `Throwable`'s constructor calls the native method `fillInStackTrace()`, which walks the *entire current call stack* at the moment of construction and records it — this is genuinely costly (proportional to stack depth) compared to the near-free table lookup used for propagation. This is why "exceptions for control flow" is a real, measurable performance anti-pattern in Java specifically: constructing a `new Exception()` pays the stack-capture cost even if it's caught one frame up, and heavily-used exceptions in hot paths sometimes override `fillInStackTrace()` to a no-op specifically to avoid this (a real, if controversial, optimization technique).

**try-with-resources desugars to an ordinary try/finally with suppressed-exception tracking.** The compiler rewrites `try (Resource r = ...) { body }` into roughly `Resource r = ...; try { body } finally { if (r != null) r.close(); }`, with one added refinement: if *both* the body and the `close()` call throw, the `close()`'s exception is not lost — it's attached to the body's exception via `addSuppressed()` and retrievable later with `getSuppressed()`, rather than silently replacing the original (more informative) exception the way a naive hand-written `finally` block historically would.

## Common Pitfalls

1. **Swallowing exceptions with an empty catch block** — `catch (Exception e) {}` silently discards the failure, turning a debuggable crash into a mysterious later symptom with no trace of the real cause.
2. **Catching `Exception` or `Throwable` too broadly** — masks genuine programming errors (`NullPointerException`, `ArrayIndexOutOfBoundsException`) alongside the specific recoverable condition you meant to handle, and can even catch `Error`s that generally should not be handled at all.
3. **A `finally` block with its own `return` or `throw`** — unconditionally overrides/discards whatever the `try`/`catch` was about to return or throw, silently losing the original outcome; this is legal Java and a genuinely confusing trap.
4. **Using exceptions for ordinary, expected control flow** — e.g., throwing to signal "not found" in a hot loop — pays the `fillInStackTrace()` cost (see Under the Hood) for something that isn't exceptional at all; return `Optional`/a sentinel/a boolean instead.
5. **Not closing resources without try-with-resources** — a hand-written `finally { conn.close(); }` is easy to get subtly wrong (e.g., forgetting the null check, or losing the original exception when `close()` itself throws); try-with-resources handles both correctly by construction.

## Quick Reference

```java
try {
    risky();
} catch (SpecificException e) {          // most specific first
    handle(e);
} catch (IOException | SQLException e) { // multi-catch, implicitly final
    handleEither(e);
} finally {
    cleanup(); // always runs
}

try (var res = openResource()) { // AutoCloseable — close() called automatically
    use(res);
}

throw new CustomException("message", causeException); // chain, don't swallow

class MyUnchecked extends RuntimeException { ... } // programmer error, no `throws` needed
class MyChecked extends Exception { ... }            // recoverable, forces `throws` or catch
```
