PrepZone Logo
PrepZone

try, catch, finally and Resources

Multi-catch, try-with-resources, propagation, and the edge cases where finally surprises you.

Read these first

Why this matters

  • finally can silently discard an exception or override a return value, and both produce bugs that are nearly impossible to spot by reading.
  • try-with-resources removed an entire category of resource leak and exception-masking bugs in one feature, and the manual equivalent is genuinely hard to write correctly.
  • The order of catch blocks is checked by the compiler in a way that trips people up exactly once.

The basic flow

try block starts
No — try completes
Yes — matching catch runs
finally always runsClose resources here
finally runs on every path — success, handled failure, or an exception on the way out. That is why cleanup belongs there, or better still in try-with-resources.
Java
try {
    int result = Integer.parseInt(input);
    System.out.println(100 / result);
} catch (NumberFormatException e) {
    System.out.println("Not a number: " + input);
} catch (ArithmeticException e) {
    System.out.println("Cannot divide by zero");
} finally {
    System.out.println("Always runs");
}
  • Execution leaves the try block at the point of failure. The remaining statements in it are skipped.
  • The first matching catch runs, and only that one. There is no fall-through.
  • finally runs after the try and any catch, whether or not an exception occurred, and even on return.
  • If no catch matches, finally still runs and the exception then propagates to the caller.

Catch order is enforced

A catch matches the exception type or any subtype, so the most specific must come first.

Java
// Correct: specific before general
try {
    Files.readString(path);
} catch (NoSuchFileException e) {        // a subtype of IOException
    System.out.println("File missing");
} catch (IOException e) {
    System.out.println("Read failed");
}
Java
// Will not compile: IOException already covers NoSuchFileException
try {
    Files.readString(path);
} catch (IOException e) {
} catch (NoSuchFileException e) {        // error: unreachable catch block
}

The compiler rejects the second form outright, which is a rare and welcome case of a mistake being impossible to ship.

Multi-catch

When several exception types need identical handling, combine them with |.

Java
try {
    process(input);
} catch (NumberFormatException | DateTimeParseException e) {
    log.warn("Malformed input: {}", e.getMessage());
    throw new ValidationException("Input could not be parsed", e);
}

The parameter is implicitly final, and its static type is the nearest common supertype — so inside the block you can only call methods available on both.

finally, and its three surprises

It runs even after return

Java
static int value() {
    try {
        return 1;
    } finally {
        System.out.println("finally ran");    // prints before the method returns 1
    }
}

The return value is computed and held, then finally runs, then the method actually returns.

A return in finally overrides everything

Java
static int broken() {
    try {
        return 1;
    } finally {
        return 2;        // the 1 is discarded
    }
}
// broken() returns 2

A return in finally swallows exceptions

Java
static int worse() {
    try {
        throw new IllegalStateException("something important");
    } finally {
        return -1;       // the exception vanishes entirely
    }
}
// worse() returns -1, and nobody ever learns about the exception

Mutating a local variable in finally does not change an already-returned primitive, because the value was copied before finally ran:

Java
static int surprising() {
    int result = 1;
    try {
        return result;        // the value 1 is captured here
    } finally {
        result = 99;          // too late
    }
}
// returns 1

When finally does not run

Only in cases where the JVM itself stops: System.exit() is called, the JVM crashes, or the thread is killed at the operating-system level. In ordinary code it always runs.

try-with-resources

Any AutoCloseable declared in the parentheses is closed automatically, correctly, in reverse order.

Java
try (BufferedReader reader = Files.newBufferedReader(input, UTF_8);
     BufferedWriter writer = Files.newBufferedWriter(output, UTF_8)) {

    reader.lines().forEach(line -> write(writer, line));

} catch (IOException e) {
    throw new UncheckedIOException("Copy failed", e);
}
AspectManual finallytry-with-resources
Lines neededNested try blocks and null checksOne declaration
Close orderYour responsibilityReverse of declaration, automatically
If close() throwsIt replaces the real exceptionRecorded as suppressed; the real one wins
If you forgetA leaked file handle or connectionNot possible
Effectively final resourcesn/aCan be referenced directly since Java 9
  • Lines needed

    Manual finallyNested try blocks and null checks
    try-with-resourcesOne declaration
  • Close order

    Manual finallyYour responsibility
    try-with-resourcesReverse of declaration, automatically
  • If close() throws

    Manual finallyIt replaces the real exception
    try-with-resourcesRecorded as suppressed; the real one wins
  • If you forget

    Manual finallyA leaked file handle or connection
    try-with-resourcesNot possible
  • Effectively final resources

    Manual finallyn/a
    try-with-resourcesCan be referenced directly since Java 9

The suppressed-exception behaviour is the part the manual version gets wrong almost every time.

The masking problem it solves is worth seeing:

Java
// The bug this feature eliminates
BufferedReader reader = null;
try {
    reader = Files.newBufferedReader(path, UTF_8);
    return reader.readLine();              // suppose this throws IOException
} finally {
    if (reader != null) reader.close();     // if this also throws, it replaces the first
}

The caller receives the close failure and never learns about the read failure. With try-with-resources the read failure propagates and the close failure is attached:

Java
catch (IOException e) {
    log.error("Primary failure", e);
    for (Throwable suppressed : e.getSuppressed()) {
        log.error("Also failed while closing", suppressed);
    }
}

Propagation

An uncaught exception unwinds the stack, frame by frame, until a matching catch is found. If none exists it reaches the thread's uncaught handler, which prints the trace and terminates that thread.

Java
void controller() {
    try {
        service();                         // handles it here
    } catch (OrderException e) {
        respondWithError(e);
    }
}

void service() throws OrderException {     // declares, does not handle
    repository();
}

void repository() throws OrderException {
    throw new OrderException("Not found");
}

Each layer makes a deliberate choice: handle, or declare and let the caller decide. The layers in between stay clean, which is the real argument in favour of exceptions over returned error codes.

Common misreadings

  • "finally does not run if the try returns." It runs after the return value is computed, before control leaves.
  • "Changing a local in finally changes the returned value." For primitives the value was already copied. For a mutable object you can still change its contents.
  • "You can order catch blocks freely." A supertype before a subtype is a compile error.
  • "try-with-resources needs an explicit finally." It generates one. Adding your own is only for extra cleanup unrelated to the resources.
  • "Closing the inner stream of a wrapped chain is necessary." Closing the outermost closes everything beneath it.
  • "A catch block must do something." It must, in practice. An empty catch is the single worst construct in Java — at minimum log it.

Quick recall

Everything you need if you only revisit this box.

  • Only the first matching catch runs, and specific types must precede general ones or the code will not compile.
  • Multi-catch with | shares one handler; the parameter is implicitly final.
  • finally runs even after return, and only System.exit() or a JVM crash prevents it.
  • Never return or throw from finally — it silently discards the real result or exception.
  • A primitive return value is captured before finally runs, so reassigning the local changes nothing.
  • try-with-resources closes in reverse order and records a failing close() as a suppressed exception, so the original failure survives.
  • Handle an exception where you have the context to act; declare it everywhere else.

Test yourself

Answer these before moving on — recall is what makes it stick.