Exceptions

This section documents the current Java release line, with Java 25 LTS as the reference point (no specific patch version is pinned), as published at the Java developer portal, the Java Tutorials, and the Java SE API specification — which are the references these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Java signals an abnormal condition by throwing an exception — an object that unwinds the call stack until a matching catch handles it, or the thread terminates. The exception’s type decides whether the compiler forces the calling code to deal with it. This page covers the class hierarchy, the try statement in all its forms, and how to define and chain your own exceptions. References: dev.java: Exceptions, the Java Tutorials Exceptions lesson, and Throwable.

The Throwable Hierarchy

Every thrown object is a Throwable, which splits into two subtrees: Error for unrecoverable JVM-level failures a program should not try to catch (OutOfMemoryError, StackOverflowError), and Exception for conditions an application can reasonably handle. RuntimeException is the one branch of Exception that is unchecked.

Throwable at the root, with Error (OutOfMemoryError, StackOverflowError) and Exception below it; Exception splits into the unchecked RuntimeException branch (NullPointerException, IllegalArgumentException) and the checked exceptions IOException and SQLException, with a dashed line marking the checked/unchecked boundary between Exception and RuntimeException

Checked exceptions — everything under Exception except the RuntimeException branch — are subject to the catch or specify requirement: a method body that can raise one must either catch it or list it in a throws clause, and the compiler enforces this. Unchecked exceptions (RuntimeException, Error, and their subclasses) carry no such obligation; they usually indicate bugs — NullPointerException, IllegalArgumentException, ArrayIndexOutOfBoundsException — that are better prevented than caught. See The Three Kinds of Exceptions and Catch or Specify Requirement.

import java.io.IOException;
import java.nio.file.*;

// checked: IOException must be caught or declared
static String readFirstLine(Path path) throws IOException {
    try (var reader = Files.newBufferedReader(path)) {
        return reader.readLine();
    }
}

// unchecked: no throws clause, the caller is not forced to handle it
static int parsePort(String text) {
    int port = Integer.parseInt(text);          // throws NumberFormatException (unchecked)
    if (port < 1 || port > 65_535) {
        throw new IllegalArgumentException("port out of range: " + port);
    }
    return port;
}

try/catch/finally and Multi-Catch

A try block is followed by zero or more catch clauses and an optional finally that runs no matter how the block exits — normal completion, a caught exception, or one propagating out. A single catch can handle several unrelated types with the | syntax. See Catching and Handling Exceptions.

import java.io.IOException;

try {
    var config = load(path);
    apply(config);
} catch (IOException | IllegalStateException e) {   // multi-catch: one handler, two types
    System.err.println("could not start: " + e.getMessage());
    throw e;                                        // e is implicitly final in a multi-catch
} catch (RuntimeException e) {
    System.err.println("unexpected: " + e);
    throw e;
} finally {
    releaseLock();                                  // always runs
}

Catch clauses are tested top to bottom, so a more specific type must come before a more general one: writing catch (Exception e) ahead of catch (IOException e) is a compile error because the second clause is unreachable.

A return, break, or continue inside finally silently discards any exception or return value in flight from the try or catch block. Never put a control-flow statement in finally. See The finally Block.

static int broken() {
    try {
        throw new IllegalStateException("boom");
    } finally {
        return -1;      // swallows the exception completely; the caller just sees -1
    }
}

try-with-resources and AutoCloseable

A resource declared in the try (…​) header is closed automatically when the block exits, in the reverse of declaration order, whether it exits normally or by exception. The resource type must implement AutoCloseable or its subinterface Closeable, whose close() is narrowed to throw only IOException. See The try-with-resources Statement.

class Resource implements AutoCloseable {
    final String name;
    Resource(String name) { this.name = name; System.out.println("open " + name); }
    void use()            { System.out.println("use " + name); }
    @Override public void close() { System.out.println("close " + name); }
}

try (var a = new Resource("A");
     var b = new Resource("B")) {      // an effectively-final local may also be listed by name
    a.use();
    b.use();
}
// open A / open B / use A / use B / close B / close A

If the try block throws and a close() also throws, the close() failure is suppressed: attached to the primary exception instead of replacing it. Retrieve suppressed exceptions with Throwable.getSuppressed().

class Faulty implements AutoCloseable {
    @Override public void close() { throw new IllegalStateException("close failed"); }
}

try {
    try (var f = new Faulty()) {
        throw new RuntimeException("work failed");   // primary exception
    }
} catch (RuntimeException e) {
    System.out.println(e.getMessage());              // work failed
    for (Throwable s : e.getSuppressed()) {
        System.out.println("  suppressed: " + s.getMessage());   // close failed
    }
}

A hand-written finally that calls close() gets this backwards: the close() failure replaces the real error and the original is lost. Always prefer try-with-resources — see also I/O and Files.

Throwing, Custom Exceptions, and Chaining

throw raises an exception; a throws clause on the method signature declares the checked types it may let propagate. A custom exception is a class extending Exception (checked — callers must handle it) or RuntimeException (unchecked). Give it at least a message constructor and a (message, cause) constructor. See How to Throw Exceptions and Specifying the Exceptions Thrown by a Method.

import java.io.IOException;
import java.nio.file.*;

class ConfigException extends Exception {                    // checked
    ConfigException(String message)                 { super(message); }
    ConfigException(String message, Throwable cause) { super(message, cause); }
}

static Config load(Path path) throws ConfigException {
    try {
        return parse(Files.readString(path));
    } catch (IOException e) {
        // chaining: keep the low-level cause, add domain context
        throw new ConfigException("cannot read config at " + path, e);
    }
}

Exception chaining preserves the original failure as the cause. Pass it to the constructor, or call initCause once; read it back with getCause(). A printed trace shows the whole chain under Caused by:. See Chained Exceptions.

Exception in thread "main" com.example.ConfigException: cannot read config at /etc/app.conf
        at com.example.Loader.load(Loader.java:19)
        at com.example.Main.main(Main.java:11)
Caused by: java.nio.file.NoSuchFileException: /etc/app.conf
        at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:92)
        ... 2 more

Read a trace bottom-up for the root cause and top-down for where it surfaced; the first line under each frame block is the call site. Programmatic access is via getStackTrace().

assert and -ea

An assert statement checks an invariant that should never be false in a correct program. Assertions are disabled by default and only execute when the JVM is started with -ea (or -enableassertions), so they are a development and testing aid, not input validation for public methods — use IllegalArgumentException or java.util.Objects.requireNonNull for that. See JLS 14.10, The assert Statement.

private int midpoint(int low, int high) {
    assert low <= high : "low > high: " + low + " > " + high;   // AssertionError when -ea and false
    return low + (high - low) / 2;
}
java -ea com.example.Main      # assertions on (tests, local runs)
java com.example.Main          # assertions off (production default)

Control Flow: try / catch / finally / Auto-Close

The order is fixed: declared resources are closed before any catch clause runs, and finally always runs last, after both. The diagram shows the no-exception path and the exception path.

flowchart TD A[Acquire resources in try header] --> B[Execute try block] B -->|no exception| C[Auto-close resources, reverse order] C --> D[Skip all catch clauses] D --> E[Execute finally block] E --> F[Statement completes normally] B -->|exception thrown| G[Auto-close resources, reverse order] G -->|a close also throws| H[Attach it as suppressed on the primary exception] G -->|closes are clean| I[Primary exception stands] H --> J{Matching catch clause?} I --> J J -->|yes| K[Run the catch block] J -->|no| L[Exception will propagate] K --> M[Execute finally block] L --> M M --> N{finally itself throws or returns?} N -->|yes| O[That outcome replaces the pending exception or return] N -->|no| P[Pending exception or return value proceeds]

See Also

  • I/O and Files — IOException, UncheckedIOException, and why streams belong in try-with-resources.

  • Optional — returning "no value" instead of throwing when absence is an ordinary outcome.

  • Records and Sealed Classes — modelling a recoverable success/failure result as data.

  • Annotations and Reflection — ReflectiveOperationException and unwrapping InvocationTargetException.