JAVA / EXCEPTIONS
finally and cleanup that always runs
Use finally to run cleanup on every exit path out of a try, and predict how return, break and thrown exceptions interact with it.
What you will learn
- Trace the exact order: return value evaluated, then finally, then the value handed back
- Use try/finally with no catch to clean up while the exception keeps propagating
- Keep return, break and throw out of finally so pending outcomes survive
- Name the cases where finally never runs, such as System.exit or a dead JVM
Understanding finally and cleanup that always runs
A finally block is attached to the exits of its try block rather than to a point in the source. The compiler arranges for every way control can leave the try — falling off the end, return, break, continue, or an exception nobody catches — to pass through the finally code first. That is what makes it the only place to put code that must run on both the success and the failure path without knowing which one happened.
The ordering matters more than beginners expect. A statement like return x first evaluates x and stores that result, then runs finally, then hands the stored value to the caller, so assigning to x inside finally changes the variable but not the answer already captured. The same machinery produces the sharp edge: an abrupt exit that starts inside finally — a return, break, continue, or throw — replaces the pending one, which is why a cleanup call that throws deletes the original exception instead of joining it.
Use finally for the things the JVM will not undo for you: releasing a lock, decrementing a depth counter, restoring a thread's interrupt flag or a previous ThreadLocal value, deleting a temp file, stopping a timer. A try/finally with no catch at all is a normal shape — you are not claiming to handle the failure, only guaranteeing state is consistent while it flies past. The guarantee is about control flow inside a running JVM, so if the try body calls System.exit or the process dies, the finally simply does not happen.
Cleanup belongs where the acquisition happened, which is why the handle must be declared before the try.
public class Main {
static int readCount(String data) {
System.out.println("open " + data);
try {
int n = Integer.parseInt(data);
System.out.println("parsed " + n);
return n;
} catch (NumberFormatException e) {
System.out.println("bad number: " + e.getMessage());
return -1;
} finally {
System.out.println("close " + data);
}
}
static void withoutCatch() {
try {
throw new IllegalStateException("disk full");
} finally {
System.out.println("cleanup runs before the throw leaves the method");
}
}
public static void main(String[] args) {
System.out.println("result " + readCount("42"));
System.out.println("result " + readCount("x7"));
try {
withoutCatch();
} catch (IllegalStateException e) {
System.out.println("caught in main: " + e.getMessage());
}
}
}finally is the block Java runs on every path out of the try, which makes it right for cleanup and wrong for control flow.
Worked examples
finally versus a pending return value
Shows that finally can change a local but not the value already captured for the return, unless it returns itself.
public class Main {
static int sneaky() {
int n = 1;
try {
return n;
} finally {
n = 99;
System.out.println("finally set n to " + n);
}
}
static int hijack() {
try {
return 1;
} finally {
return 99;
}
}
public static void main(String[] args) {
System.out.println("sneaky -> " + sneaky());
System.out.println("hijack -> " + hijack());
}
}Example explained
Line 1return n evaluates n to 1 and stores that value, so the caller's answer is fixed before finally starts.
Line 2n = 99 really does change the local, as the print proves, but the stored return value is a separate copy.
Line 3return 99 in hijack is a new abrupt exit begun inside finally, so it overwrites the pending return 1.
Line 4Both methods compile without error; the second is legal Java and exactly the pattern lint tools warn about.
cleanup that throws erases the real failure
Demonstrates that an exception thrown from finally replaces the original exception with no link between them.
public class Main {
static void run() {
try {
throw new IllegalArgumentException("original failure");
} finally {
throw new IllegalStateException("cleanup failure");
}
}
public static void main(String[] args) {
try {
run();
} catch (RuntimeException e) {
System.out.println("caught: " + e.getClass().getSimpleName() + ": " + e.getMessage());
System.out.println("suppressed: " + e.getSuppressed().length);
}
}
}Example explained
Line 1The try body throws IllegalArgumentException, which becomes the pending exception on the way out.
Line 2The throw inside finally starts a fresh abrupt exit, so it replaces the pending one rather than wrapping it.
Line 3getSuppressed() has length 0: nothing connects the two, so the original cause is gone from the stack trace.
Line 4Close, unlock and flush calls can fail the same way, so put them in their own try/catch inside the finally.
finally on break and continue
Shows that leaving the try with continue or break also runs the finally block.
public class Main {
public static void main(String[] args) {
for (int i = 1; i <= 3; i++) {
try {
if (i == 2) continue;
if (i == 3) break;
System.out.println("body " + i);
} finally {
System.out.println("finally " + i);
}
}
System.out.println("after loop");
}
}Example explained
Line 1Only i == 1 completes the try normally, so it is the single iteration that prints a body line.
Line 2i == 2 leaves the try with continue, and finally 2 still prints before the next iteration starts.
Line 3i == 3 leaves the try and the loop with break, and finally 3 prints before control reaches the line after the loop.
Important notes
finally is a control-flow guarantee, not a process guarantee: System.exit in the try body, a JVM crash, an endless loop or a killed process all leave it unrun.
For anything that implements AutoCloseable, the resource form of try handles close ordering and exception suppression; keep finally for state you own, such as locks and counters.
Common mistakes
Putting return or break in finally: it replaces the pending exception, so a failed operation reports success and the stack trace disappears.
Calling cleanup that can throw directly in finally: the cleanup exception overwrites the original, and the debugging session goes after the wrong error.
Declaring the handle inside the try and using it in finally: that does not compile, and declaring it outside without a null check throws NullPointerException from the finally when acquisition failed.
Try it yourself
Change, predict, then run
Write a static int depth field and a method dig(int n) that prints depth spaces plus n, increments depth, recurses with n - 1, throws IllegalStateException when n reaches 0, and decrements depth in a finally. Call dig(3) from main inside a try/catch and print depth afterwards to confirm it is back to 0.
Open the Java workspaceCheck your understanding
A method contains int x = 1; try { return x; } finally { x = 99; } and it returns 1. What explains that?
- finally cannot see or modify the local variables of the enclosing method.
- The compiler hoists the finally block above the return statement.
- The return expression is evaluated and its result stored before finally runs, so the later assignment is not part of the answer.
- x becomes effectively final once a try returns it, so the assignment is silently ignored.
Show answer
The value given to the caller is captured the moment return x executes; finally runs afterwards on the way out, so it can change x but not the already stored result. The first option is tempting because the effect looks like the assignment was blocked, yet a print inside the finally shows x is genuinely 99 — only a new abrupt exit, such as return 99 inside finally, can replace the pending value.