thecodex.expert · The Codex Family of Knowledge
Java

Exception Handling in Java

Compile-time enforcement of error handling — Java's exception system forces you to think about failure paths.

Java 21 LTS OpenJDK Last verified:
Canonical Definition

Java exception handling uses a class hierarchy rooted at Throwable — checked exceptions (extend Exception) must be declared or caught at compile time, unchecked exceptions (extend RuntimeException) may propagate freely, and try-with-resources automatically closes AutoCloseable resources.

One sentence

Java exceptions are objects that represent errors — checked exceptions must be handled at compile time, unchecked exceptions can propagate freely, and try-with-resources ensures resources are always closed.

The exception hierarchy

All exceptions in Java extend Throwable. The two branches matter: Error (JVM-level — don't catch) and Exception (application-level — handle these).

ClassTypeMust handle?Examples
ErrorUncheckedNo (don't try)OutOfMemoryError, StackOverflowError
RuntimeExceptionUncheckedNoNullPointerException, IllegalArgumentException, IndexOutOfBoundsException
Exception (other)CheckedYes — throws or catchIOException, SQLException, ParseException
Javaexception_basics.java
// try-catch-finally
try {
    int result = 10 / 0;          // throws ArithmeticException
} catch (ArithmeticException e) {
    System.err.println("Error: " + e.getMessage());  // "/ by zero"
} finally {
    // always runs — cleanup, logging
    System.out.println("Done");
}

// Multiple catch blocks
try {
    String s = null;
    int n = Integer.parseInt(s);   // NullPointerException
} catch (NumberFormatException e) {
    System.err.println("Not a number");
} catch (NullPointerException e) {
    System.err.println("Null input");
} catch (Exception e) {
    System.err.println("Unexpected: " + e.getClass().getSimpleName());
}
// More specific exceptions must come BEFORE more general ones

// Multi-catch (Java 7+) — when handling is identical
try {
    process();
} catch (IOException | SQLException e) {
    log.error("Data error: {}", e.getMessage());
}

Checked vs unchecked

Javachecked_unchecked.java
// CHECKED exception — extends Exception (not RuntimeException)
// Compiler requires you to either catch it or declare throws

// Method that declares it throws checked exception
public String readFile(Path path) throws IOException {
    return Files.readString(path);   // Files.readString throws IOException
}

// Caller must handle:
try {
    String content = readFile(Path.of("data.txt"));
} catch (IOException e) {
    System.err.println("Could not read file: " + e.getMessage());
}

// UNCHECKED exception — extends RuntimeException
// No obligation to declare or catch (but can still catch)
public int divide(int a, int b) {
    if (b == 0) throw new ArithmeticException("Divisor cannot be zero");
    return a / b;
}

// Throwing checked exceptions from lambdas requires wrapping
// (lambdas cannot declare checked exceptions)
List<Path> paths = List.of(Path.of("a.txt"), Path.of("b.txt"));
paths.forEach(p -> {
    try {
        System.out.println(Files.readString(p));
    } catch (IOException e) {
        throw new RuntimeException(e);   // wrap in unchecked
    }
});

try-with-resources

Javatry_with_resources.java
// try-with-resources — automatically closes AutoCloseable resources
// Resource declared in the () is closed when the try block exits
// (whether normally, via exception, or via return)

// Old style — error-prone (what if close() itself throws?)
BufferedReader br = null;
try {
    br = new BufferedReader(new FileReader("file.txt"));
    String line = br.readLine();
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (br != null) {
        try { br.close(); } catch (IOException e) { /* swallow */ }
    }
}

// Modern style — clean and correct
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException e) {
    System.err.println("Read error: " + e.getMessage());
}
// br.close() called automatically — even if exception was thrown

// Multiple resources — closed in reverse order of declaration
try (var conn = dataSource.getConnection();
     var stmt = conn.prepareStatement("SELECT * FROM users")) {
    ResultSet rs = stmt.executeQuery();
    while (rs.next()) System.out.println(rs.getString("name"));
} catch (SQLException e) {
    throw new RuntimeException("DB query failed", e);
}

Custom exceptions

Javacustom_exceptions.java
// Custom checked exception — extend Exception
public class InsufficientFundsException extends Exception {
    private final double amount;
    private final double balance;

    public InsufficientFundsException(double amount, double balance) {
        super(String.format("Cannot withdraw %.2f: balance is %.2f", amount, balance));
        this.amount  = amount;
        this.balance = balance;
    }

    public double getAmount()  { return amount; }
    public double getBalance() { return balance; }
}

// Custom unchecked exception — extend RuntimeException
public class DatabaseUnavailableException extends RuntimeException {
    public DatabaseUnavailableException(String message, Throwable cause) {
        super(message, cause);
    }
}

// Using custom exceptions
class BankAccount {
    private double balance;

    public void withdraw(double amount) throws InsufficientFundsException {
        if (amount > balance) {
            throw new InsufficientFundsException(amount, balance);
        }
        balance -= amount;
    }
}

// Exception chaining — preserve the original cause
try {
    dbOperation();
} catch (SQLException e) {
    // Wrap in domain exception, preserving the cause
    throw new DatabaseUnavailableException("User lookup failed", e);
}
// getCause() on the outer exception returns the original SQLException

Exception best practices

Javaexception_practices.java
// 1. Catch specific, not general
// WRONG:
try { process(); } catch (Exception e) { /* swallows everything */ }

// RIGHT:
try { process(); }
catch (FileNotFoundException e) { createDefaultConfig(); }
catch (IOException e)           { log.error("I/O error", e); throw e; }

// 2. Never swallow exceptions silently
// WRONG:
try { risky(); } catch (Exception e) {}  // hides bugs

// RIGHT:
try { risky(); } catch (Exception e) { log.error("risky() failed", e); }

// 3. Include context in the message
// WRONG:
throw new IllegalArgumentException("Invalid input");
// RIGHT:
throw new IllegalArgumentException(
    "userId must be positive, got: " + userId);

// 4. Preserve the cause when wrapping
// WRONG:
catch (IOException e) { throw new ServiceException("Failed"); }  // cause lost
// RIGHT:
catch (IOException e) { throw new ServiceException("Failed", e); }

// 5. try-with-resources for anything Closeable — always

// 6. Document with @throws Javadoc
/**
 * Withdraws the given amount.
 * @throws InsufficientFundsException if balance < amount
 * @throws IllegalArgumentException   if amount <= 0
 */
public void withdraw(double amount) throws InsufficientFundsException { ... }
Commonly confused
Catching Exception catches checked exceptions but NOT Error. Error is a sibling of Exception, not a subclass. Catching Exception will not catch OutOfMemoryError or StackOverflowError. To catch everything (rarely correct), catch Throwable.
finally always runs — even after return. If a try block has a return statement AND a finally block, the finally block runs before the method actually returns. If finally also has a return statement, it overrides the try block's return value — a notorious source of bugs. Never put return in finally.
Checked exceptions cannot propagate through lambdas without wrapping. The SAM (Single Abstract Method) of most functional interfaces (Runnable, Predicate, Function) does not declare checked exceptions. If your lambda body throws a checked exception, you must either catch it inside the lambda or wrap it in a RuntimeException.

The exception specification and suppressed exceptions

JLS §11 defines the Java exception model. When an exception is thrown, the JVM searches the call stack for a handler — a matching catch block — unwinding frames until one is found or the thread terminates. The try-with-resources statement (JLS §14.20.3) calls close() on resources in reverse declaration order. If both the try body and a close() call throw, the original exception is rethrown and the close() exception is attached as a suppressed exception via Throwable.addSuppressed(). Suppressed exceptions are visible in stack traces and accessible via getSuppressed() — no exception is silently lost.

JEP 358 — Helpful NullPointerExceptions (Java 14)

Before Java 14, a NullPointerException stack trace only showed the line number, leaving you to guess which of multiple possible dereferences caused it. JEP 358 (Java 14, enabled by default in Java 15+) adds a descriptive message: Cannot invoke "String.length()" because "str" is null or Cannot read field "address" because "user" is null. This is implemented by the JVM analysing the bytecode at the throw site to reconstruct which specific operation failed.

Sources

1
Java Language Specification §11 — Exceptions. docs.oracle.com/javase/specs/jls/se21/html/jls-11.html.
2
Java Language Specification §14.20 — The try Statement. docs.oracle.com/javase/specs/jls/se21/html/jls-14.html#jls-14.20.
3
JEP 358 — Helpful NullPointerExceptions. openjdk.org/jeps/358.
4
Java SE 21 API — java.lang.Throwable. docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Throwable.html.
Source confidence: High Last verified: Primary source: Java SE 21 Documentation · docs.oracle.com/en/java/

Sources

1
JLS §11 — Exceptions. docs.oracle.com/javase/specs/jls/se21/html/jls-11.html.
2
JLS §14.20 — The try Statement. docs.oracle.com/javase/specs/jls/se21/html/jls-14.html#jls-14.20.
3
JEP 358 — Helpful NullPointerExceptions. openjdk.org/jeps/358.
4
Java SE 21 API — Throwable. docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Throwable.html.