Exceptions

73. Checked vs Unchecked Exceptions

Checked exceptions must be declared or handled at compile time; unchecked exceptions (RuntimeException and its subclasses) do not. The design tradeoff: checked exceptions force callers to consider failure modes but pollute APIs.

Java 21 (core language)

Explanation

The Java compiler enforces that checked exceptions (those that extend Exception but not RuntimeException) are either caught in a try-catch or declared in the method's throws clause. This is Java's way of encoding "this operation can fail in a recoverable, caller-relevant way" directly in the type system.

Unchecked exceptions extend RuntimeException (e.g., NullPointerException, IllegalArgumentException, IndexOutOfBoundsException). They represent programming errors or conditions from which callers typically cannot recover โ€” they are not required to be declared or caught.

Design guidance (Effective Java): use checked exceptions for conditions from which the caller can reasonably be expected to recover. Use unchecked exceptions for programming errors. Avoid checked exceptions if you cannot provide useful recovery information. Many modern APIs (Spring, libraries, and modern Java itself) trend toward unchecked exceptions because checked exceptions compose poorly with streams and lambdas.

Example

import java.io.*;
import java.util.List;

public class CheckedVsUnchecked {
    // Checked exception: caller must handle or declare
    static String readFile(String path) throws IOException {
        return new String(new FileInputStream(path).readAllBytes());
    }

    // Unchecked exception: documents the precondition, no throws clause needed
    static int divide(int a, int b) {
        if (b == 0) throw new IllegalArgumentException("Divisor cannot be zero");
        return a / b;
    }

    // Modern pattern: wrap checked in unchecked for stream-friendly APIs
    @FunctionalInterface
    interface CheckedSupplier<T> {
        T get() throws Exception;
    }

    static <T> T sneakyGet(CheckedSupplier<T> supplier) {
        try { return supplier.get(); }
        catch (Exception e) { throw new RuntimeException(e); }
    }

    public static void main(String[] args) {
        // Checked: must handle
        try {
            String content = readFile("/nonexistent.txt");
        } catch (IOException e) {
            System.out.println("IO error (expected): " + e.getMessage());
        }

        // Unchecked: optional catch
        try {
            divide(10, 0);
        } catch (IllegalArgumentException e) {
            System.out.println("Caught unchecked: " + e.getMessage());
        }

        // Checked exception inside stream โ€” wrapper needed
        List<String> paths = List.of("/tmp/a.txt", "/tmp/b.txt");
        paths.stream()
             .map(p -> sneakyGet(() -> readFile(p))) // wraps IOException
             .forEach(System.out::println);
    }
}

Sources