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.
try {
// 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
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.
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 nullfinally 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.
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 bugreturn 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.
@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