Java finally Block
What the finally Block Does
A finally block contains code that runs after the try block, whether or not an exception occurred, and whether or not it was caught. It is attached to a try statement, after any catch blocks:
try {
// code that may throw
} catch (SomeException e) {
// optional: handle the exception
} finally {
// always runs when the try statement is left
}
Its purpose is cleanup: releasing something that was acquired before or inside the try — a lock, a file handle, a database connection, a counter, a temporary state change. Cleanup code must run on every path out of the block. Without finally, you would have to copy it into the end of the try, into every catch, and before every return, and an unexpected exception type would still skip it.
A try statement must have at least one catch or a finally. So three forms are valid: try-catch, try-catch-finally and try-finally.
The Three Paths Through try-catch-finally
public class Paths {
static void process(String input) {
System.out.println("process(\"" + input + "\")");
try {
int n = Integer.parseInt(input);
System.out.println(" 10 / " + n + " = " + (10 / n));
} catch (NumberFormatException e) {
System.out.println(" catch: not a number");
} finally {
System.out.println(" finally: runs");
}
System.out.println(" after the statement");
}
public static void main(String[] args) {
process("5");
process("five");
try {
process("0");
} catch (ArithmeticException e) {
System.out.println("main caught: " + e.getMessage());
}
}
}
Output:
process("5")
10 / 5 = 2
finally: runs
after the statement
process("five")
catch: not a number
finally: runs
after the statement
process("0")
finally: runs
main caught: / by zero
| Input | What happens in the try | catch | finally | After the statement |
|---|---|---|---|---|
"5" | Completes normally | Skipped | Runs | Runs |
"five" | NumberFormatException | Runs | Runs after the catch | Runs |
"0" | ArithmeticException, which no catch here matches | Skipped | Runs | Skipped — the exception continues to propagate |
The third case is the most important. The method can't handle division by zero, so the exception leaves process — but before it does, the JVM runs the finally block. Once finally finishes, the exception continues propagating as if nothing had interrupted it, and main catches it. "after the statement" isn't printed because the method was exited by the exception.
So finally doesn't handle exceptions; it just guarantees its code runs on the way out.
try-finally Without catch
When a method can't do anything useful about an exception but still has to clean up, use try-finally without a catch. The exception propagates to the caller, and the cleanup still happens.
This example uses a ReentrantLock, a lock from java.util.concurrent.locks that lets only one thread at a time run the protected code. If a lock is acquired and never released, every other thread that needs it waits forever, so releasing it is exactly the kind of cleanup finally exists for.
import java.util.concurrent.locks.ReentrantLock;
public class Bank {
private final ReentrantLock lock = new ReentrantLock();
private long balance = 5_000;
void withdraw(long amount) {
lock.lock();
try {
if (amount > balance) {
throw new IllegalArgumentException("insufficient funds: balance " + balance + ", requested " + amount);
}
balance -= amount;
System.out.println("withdrew " + amount + ", balance " + balance);
} finally {
lock.unlock();
System.out.println(" lock released, still held: " + lock.isLocked());
}
}
public static void main(String[] args) {
Bank account = new Bank();
account.withdraw(1_200);
try {
account.withdraw(9_000);
} catch (IllegalArgumentException e) {
System.out.println("rejected: " + e.getMessage());
}
account.withdraw(800);
}
}
Output:
withdrew 1200, balance 3800
lock released, still held: false
lock released, still held: false
rejected: insufficient funds: balance 3800, requested 9000
withdrew 800, balance 3000
lock released, still held: false
How it works:
lock.lock()is called before thetry, not inside it. If it were inside andlock()itself failed, thefinallywould callunlock()on a lock this thread never acquired, which throwsIllegalMonitorStateExceptionand hides the original problem. Acquire first, then entertry— this is the standard idiom recommended in theLockdocumentation.- On the successful withdrawal, the
finallyreleases the lock after the balance changes. - On the failed withdrawal of 9,000, the
IllegalArgumentExceptionis thrown.withdrawhas nocatch, so thefinallyruns (second "lock released" line), and then the exception reachesmain, which prints "rejected". Notice the order in the output: cleanup happens before the caller sees the exception. - The third withdrawal works, proving that the lock was really released after the failure. Without
finally, the failed call would have left the lock held, and in a multi-threaded application every later withdrawal from other threads would block forever.
finally and return
A finally block runs even when the try or catch block leaves the method with return. The exact order is subtle:
- The
returnexpression is evaluated and the value is saved. - The
finallyblock runs. - The method returns the saved value.
public class Ret {
static int counter() {
int x = 1;
try {
return x;
} finally {
x = 99;
System.out.println("finally sets x = " + x);
}
}
static StringBuilder builder() {
StringBuilder sb = new StringBuilder("order");
try {
return sb;
} finally {
sb.append(" #42");
}
}
public static void main(String[] args) {
System.out.println("counter() returned " + counter());
System.out.println("builder() returned " + builder());
}
}
Output:
finally sets x = 99
counter() returned 1
builder() returned order #42
- In
counter(), the value1is saved whenreturn xexecutes. Changing the variablexinfinallydoesn't affect the saved value, so the method returns1. - In
builder(), the saved value is a reference to theStringBuilderobject.finallydoesn't replace the reference; it modifies the object it points to. The caller receives the same object, now containing"order #42".
This is the same rule as Java's pass-by-value semantics: primitives are copied, while for objects the reference is copied and the object itself is shared. In practice, finally should only release resources; changing returned data there makes code very hard to follow.
Never Return from finally
A return (or break, continue or throw) inside finally replaces whatever the try or catch was doing — including an exception in flight:
public class Swallow {
@SuppressWarnings("finally")
static int risky() {
try {
throw new IllegalStateException("database is down");
} finally {
return -1;
}
}
public static void main(String[] args) {
System.out.println("risky() = " + risky());
}
}
Output:
risky() = -1
The IllegalStateException disappears completely. The caller gets -1 and never learns the database is down, and no stack trace is printed anywhere. The compiler can warn about this; compiled with javac -Xlint:all and without the @SuppressWarnings annotation, it reports:
warning: [finally] finally clause cannot complete normally
A finally block should always be able to finish normally ("complete normally"), so that the original outcome of the try — a value or an exception — is preserved.
An Exception in finally Hides the Original
The same problem occurs when the cleanup code itself throws:
public class Mask {
public static void main(String[] args) {
try {
try {
throw new IllegalStateException("payment failed");
} finally {
throw new RuntimeException("cleanup failed");
}
} catch (RuntimeException e) {
System.out.println("Caught: " + e.getMessage());
}
}
}
Output:
Caught: cleanup failed
The important exception — "payment failed" — is lost; only the cleanup failure reaches the handler. In real code this happens with close() methods: a stream fails while writing, and then close() also fails in finally, replacing the first, more useful exception.
There are two ways to deal with this:
- Wrap risky cleanup in its own
try...catchinsidefinally, logging the cleanup failure instead of letting it escape. - For anything that implements
AutoCloseable(files, streams, sockets, JDBC connections), use try-with-resources. It closes resources automatically and, if closing fails after an earlier exception, attaches the close failure as a suppressed exception instead of losing the original. This is covered in the Try With Resources lesson, and it has replaced manualfinallyblocks for closing resources since Java 7.
When finally Does Not Run
finally runs on every normal way of leaving the try statement — finishing, return, break, continue or an exception. It does not run if the JVM stops:
public class Exit {
public static void main(String[] args) {
try {
System.out.println("shutting down");
System.exit(0);
} finally {
System.out.println("finally");
}
}
}
Output:
shutting down
System.exit terminates the JVM immediately (after running shutdown hooks), so "finally" is never printed. The same is true if the process is killed (kill -9), the machine loses power or the JVM crashes. Similarly, if the try contains an infinite loop or a thread blocks forever, the finally never gets its turn.
This means finally is a guarantee within a running program, not a guarantee against crashes. Data that must survive a crash belongs in a transaction or a durable store, not in cleanup code.
Practical Example: Keeping a Counter Accurate
Web servers often track how many requests are currently in progress for monitoring dashboards. If the counter is incremented at the start and decremented only on success, every failed request leaves it one too high, and the dashboard drifts upward forever.
import java.util.*;
public class InFlight {
static final Map<String, Integer> active = new TreeMap<>();
static String handle(String path) {
active.merge(path, 1, Integer::sum);
try {
if (path.startsWith("/admin")) {
throw new SecurityException("forbidden: " + path);
}
return "200 OK " + path;
} finally {
active.merge(path, -1, Integer::sum);
}
}
public static void main(String[] args) {
for (String p : List.of("/home", "/admin/users", "/cart")) {
try {
System.out.println(handle(p));
} catch (SecurityException e) {
System.out.println("403 " + e.getMessage());
}
}
System.out.println("in-flight requests: " + active);
}
}
Output:
200 OK /home
403 forbidden: /admin/users
200 OK /cart
in-flight requests: {/admin/users=0, /cart=0, /home=0}
active.merge(path, 1, Integer::sum)adds 1 to the path's count (starting from 1 if the path isn't in the map yet).- The successful requests leave
handlethroughreturn; the forbidden one leaves through an exception. Thefinallyblock decrements the counter on both paths. - At the end every counter is back to
0, which is the correct "nothing in progress" state. Move the decrement to just beforereturnand/admin/userswould stay at1.
The same structure appears throughout real systems: start a timer and record the duration in finally, set a "busy" flag and clear it in finally, set a logging context (such as SLF4J's MDC.put) and remove it in finally so it doesn't leak into the next request handled by the same thread.
Manual Resource Closing with finally
Before Java 7, closing a file required a finally block like this:
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("orders.csv"));
System.out.println(reader.readLine());
} finally {
if (reader != null) {
reader.close(); // can itself throw IOException
}
}
The null check is needed because if the constructor fails, reader is never assigned and there is nothing to close. You'll see this pattern in older code bases. In new code, the equivalent is shorter and safer:
try (BufferedReader reader = new BufferedReader(new FileReader("orders.csv"))) {
System.out.println(reader.readLine());
}
Use finally for cleanup that isn't an AutoCloseable resource — unlocking, restoring a flag, decrementing a counter, recording metrics — and try-with-resources for anything that has a close() method.