Skip to content
Part IA Michaelmas Term

Throwing, Catching and Try-With-Resources

The throw Statement

throw creates and throws an exception object:

throw new IllegalArgumentException("age must be non-negative, got: " + age);

The exception object is allocated on the heap like any other Java object. The JVM captures a stack trace at the point of the throw, recording the current call stack — which methods were active, at which line numbers. This trace is stored in the exception and is invaluable for debugging.

Once thrown, the JVM begins unwinding the stack: it exits the current method (and every caller, in succession) until it finds a matching catch block. If no catch block is found in any stack frame, the thread terminates and the uncaught exception is printed to System.err.

The try-catch Block

try {
    String text = Files.readString(Path.of("data.txt"));
    process(text);
} catch (NoSuchFileException e) {
    System.err.println("File not found: " + e.getFile());
} catch (IOException e) {
    System.err.println("I/O error: " + e.getMessage());
}

Key rules:

  • catch blocks are checked in order. The first matching type handles the exception.
  • More specific types must come before more general ones. catch (IOException e) before catch (Exception e) — otherwise the specific catch is unreachable and the compiler rejects the code.
  • The variable e in the catch block receives a reference to the thrown exception object. You can inspect its message, cause, and stack trace.

Multi-Catch (Union Catch)

Since Java 7, you can catch multiple unrelated exception types with a single handler:

try {
    doSomething();
} catch (IOException | SQLException e) {
    System.err.println("Operation failed: " + e.getMessage());
}

The variable e is implicitly final in a multi-catch block — you cannot assign to it. The exception types in the union must be disjoint (no type in the list is a subtype of another in the list).

The finally Block

The finally block always runs — regardless of whether:

  • The try block completes normally.
  • The try block throws an exception (caught or uncaught).
  • The try block executes a return, break, or continue.
FileInputStream fis = null;
try {
    fis = new FileInputStream("data.txt");
    int b;
    while ((b = fis.read()) != -1) {
        process((byte) b);
    }
} catch (IOException e) {
    System.err.println("Error: " + e.getMessage());
} finally {
    if (fis != null) {
        try {
            fis.close();
        } catch (IOException e) {
            System.err.println("Failed to close file");
        }
    }
}

The finally block is used for cleanup that must happen regardless — closing files, releasing locks, freeing native resources.

Important edge case: if the try block throws an exception and the finally block also throws an exception, the finally exception replaces the try exception — the original exception is lost. This is one reason try-with-resources (below) is preferred: it preserves the original exception and attaches the close exception as a suppressed exception.

Try-With-Resources (Java 7+)

Try-with-resources eliminates the verbose finally-close pattern:

try (FileReader fr = new FileReader("data.txt");
     BufferedReader br = new BufferedReader(fr)) {

    String line = br.readLine();
    System.out.println(line);

}

Resources are closed automatically when the try block exits — even if an exception occurs. Key properties:

  • Resources must implement AutoCloseable (or the older Closeable).
  • Multiple resources separated by semicolons are closed in reverse order of declaration (br then fr).
  • If the try body throws and close() also throws, the close exception is suppressed and attached to the main exception via getSuppressed():
try (FailingResource r = new FailingResource()) {
    throw new Exception("try body failed");
} catch (Exception e) {
    System.err.println("Main: " + e.getMessage());
    for (Throwable suppressed : e.getSuppressed()) {
        System.err.println("Suppressed: " + suppressed.getMessage());
    }
}

AutoCloseable versus Closeable

  • AutoCloseable.close() may throw Exception.
  • Closeable extends AutoCloseable, narrowing close() to throw IOException instead — specifically for I/O resources.

In practice, use AutoCloseable for general-purpose resources and Closeable for I/O.

The try-only Block (No catch or finally)

A try block with resources but without catch or finally is valid:

try (BufferedReader br = Files.newBufferedReader(Path.of("data.txt"))) {
    String line = br.readLine();
    System.out.println(line);
}
// br.close() is called automatically; if it throws, the exception propagates

This is the cleanest form when you have nothing useful to do with the checked exceptions — you simply declare them in your method’s throws clause and let them propagate.

The throws Declaration

A method declares the checked exceptions it might propagate:

void readFile(String path) throws IOException {
    String text = Files.readString(Path.of(path));
    process(text);
}

The throws clause is a contract with callers: “if you call this method, you must either handle IOException or propagate it yourself.” The compiler enforces this for checked exceptions only. Unchecked exceptions (subclasses of RuntimeException and Error) can be thrown without being declared.