try, catch and finally

Catch failures where you can actually handle them, release things in finally, and avoid the finally traps that hide real errors.

Syntaxtry { // code that may throw } catch (SpecificException e) { // recover, translate or respond } finally { // cleanup that always runs }

When code inside a try block throws, Java abandons the rest of that block and looks for the first catch clause whose type matches the exception. If one matches, its body runs and execution continues after the whole statement. If none matches, the exception keeps travelling up the call stack to the caller. A finally block runs in every case: after normal completion, after a catch, after a return, and while an uncaught exception is on its way out.

try {
    int port = Integer.parseInt(raw);
    return port;
} catch (NumberFormatException e) {
    return 8080;              // a sensible default for bad config
} finally {
    log("checked port value"); // runs on both paths
}

Catch order matters

Clauses are tried top to bottom, so a subclass must come before its superclass. NumberFormatException extends IllegalArgumentException; catching the parent first makes the child clause unreachable, and the compiler rejects it. Put the specific cases first and the broad fallback last.

Catch only what you can handle

The most common mistake is catching too early. A repository method that catches an exception, prints it and returns null has turned a clear failure into a NullPointerException three layers away. Catch an exception when you can do something useful: retry, fall back to a default, translate it into a domain error, or turn it into an HTTP response. Otherwise let it propagate to a single place near the edge of the service, such as the request handler, that logs it once and answers with a 500.

Empty catch blocks are worse still. If you deliberately ignore an exception, say why in a comment and keep the type narrow. catch (Exception e) {} also swallows NullPointerException and every other bug in the block.

What finally is for

finally is for cleanup that must happen whatever the outcome: releasing a lock, decrementing an in-flight request counter, clearing a per-request context. For anything that implements AutoCloseable (streams, JDBC connections, HTTP clients) prefer try-with-resources, which writes the cleanup for you and handles failures during close.

Never return or throw from inside finally. A return there replaces whatever the try was doing, including an exception in flight, so a failed database write can be reported to the client as a success. Keep finally blocks short and make sure they cannot throw themselves.

Exceptions are not free: building one fills in a stack trace. That cost is irrelevant for genuine failures, but do not use them for ordinary control flow such as checking whether a string is numeric inside a hot loop.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Fall back to a default port or timeout when a configuration value cannot be parsed.
  • Decrement an in-flight request gauge in finally so metrics stay correct when a handler throws.
  • Turn a NumberFormatException from a path parameter into a 400 response instead of a 500.

More examples

Specific catch clauses before general ones

Java picks the first clause that matches, so the narrow NumberFormatException handler sits above IllegalArgumentException. Swapping those two clauses is a compile error, because the subclass clause could never run.

Example · java
String classify(String input) {
    try {
        if (input == null) {
            throw new NullPointerException("input is null");
        }
        int n = Integer.parseInt(input);
        if (n < 0) {
            throw new IllegalArgumentException("negative: " + n);
        }
        return "200 ok " + n;
    } catch (NumberFormatException e) {
        // A subclass of IllegalArgumentException, so it must come first.
        return "400 not a number";
    } catch (IllegalArgumentException e) {
        return "400 " + e.getMessage();
    } catch (RuntimeException e) {
        return "500 " + e.getMessage();
    }
}

void main() {
    String[] inputs = {"42", "abc", "-5", null};
    for (String s : inputs) {
        IO.println(classify(s));
    }
}

// Output:
// 200 ok 42
// 400 not a number
// 400 negative: -5
// 500 input is null

finally runs on return and on an uncaught exception

The counter goes back down on both paths, which is exactly what you want for in-flight request metrics or a semaphore permit. Notice the finally line prints before the caller sees the return value or the exception.

Example · java
int inFlight = 0;

String handle(String path) {
    inFlight++;
    try {
        if (path.equals("/crash")) {
            throw new IllegalStateException("handler bug");
        }
        return "200 " + path;
    } finally {
        // Runs whether the method returns or throws.
        inFlight--;
        IO.println("released, in flight = " + inFlight);
    }
}

void main() {
    IO.println(handle("/users"));
    try {
        handle("/crash");
    } catch (IllegalStateException e) {
        IO.println("caught: " + e.getMessage());
    }
}

// Output:
// released, in flight = 0
// 200 /users
// released, in flight = 0
// caught: handler bug

return in finally swallows the exception

The disk-full failure disappears and the caller is told the order was saved. javac warns about this with -Xlint:finally; treat that warning as an error in your build.

Example · java
@SuppressWarnings("finally")
String saveOrder() {
    try {
        throw new IllegalStateException("disk full");
    } finally {
        // This return discards the exception above.
        return "saved";
    }
}

void main() {
    IO.println(saveOrder());
}

// Output:
// saved

Discussion

  • Be the first to comment on this lesson.