AlgoMaster Logo

finally Block

High Priority24 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

A try block runs code that might fail. A catch block reacts when it does. The finally block runs cleanup that needs to happen either way: closing a connection, releasing a lock, writing an audit line. This lesson covers what finally is, how it interacts with try and catch, the execution order in each scenario, the rare cases where it doesn't run, and two well-known traps (return and throw inside finally) that turn helpful cleanup into silent bugs.

Why finally Exists

Cleanup is the part of a method that runs no matter how the method ends. If an order-processing routine opens a database connection, it has to close that connection whether the order saves successfully, fails with a parse error, or throws a NullPointerException halfway through. Without a language feature for cleanup, every method would need duplicated close calls along every exit path: one in the happy path, one in each catch, and one for any uncaught exception that propagates out. Missing a single one leaks resources until the program crashes.

The finally block solves this by attaching cleanup to the try itself. Code in finally runs after the try body finishes, regardless of how it finished: normal completion, an exception caught by a matching catch, an exception that has no matching catch and is on its way out, or even a return from inside the try.

The shape of the construct:

The try body runs, then the finally body runs, then control continues past the construct. Nothing dramatic happened, because nothing failed. The interesting cases are the ones where something does.

Syntax Variants

There are three legal shapes for the construct, and finally is allowed in two of them:

ShapeLegal?When to use it
try / catchYesThe exception is handled and cleanup isn't needed.
try / finallyYesCleanup is needed but the exception should propagate.
try / catch / finallyYesThe exception is handled and cleanup is needed.
try aloneNoA try must be followed by catch, finally, or both.

The try / finally form is worth pausing on. It looks unusual because there's no catch. The block says: the exception isn't being handled here, but cleanup must still run before it propagates. The exception travels up the call stack after finally runs.

doWork doesn't catch the exception. The finally still runs, then the exception propagates to main, where the catch handles it. The cleanup happened on the way out, exactly when it needed to.

Execution Order: A Concrete Trace

The clearest way to learn finally is to watch the four common scenarios print their order. Each one starts with a "before try" line, runs through the construct, and ends with an "after" line. The differences in between are what matters.

Scenario 1: No exception

The try body completes normally, the catch is skipped because nothing was thrown, the finally runs, and execution continues past the construct.

Scenario 2: Exception thrown and caught

The throw interrupts the try. The matching catch runs, then the finally runs, then control falls through to the line after the construct. The "after" line still prints, because the exception is fully handled.

Scenario 3: Exception thrown but not caught

There's no catch, so the exception propagates. The finally block still runs before the exception leaves the method. The "after" line never prints, because the exception terminates main before it gets there.

Scenario 4: Exception thrown and a different type caught

The thrown RuntimeException doesn't match the ArithmeticException catch. The catch is skipped. The finally runs. The exception propagates and main ends.

Across all four scenarios, one rule holds: the `finally` block runs. This is the property the construct exists to provide, and the reason cleanup belongs there instead of duplicated along every exit path.

A Diagram of the Always-Runs Paths

The four scenarios share a single shape. The finally block sits on every path out of the try, no matter which path is taken.

The two arrows leaving the finally block are the heart of the construct. If no exception is still in flight after the finally runs (because the try finished normally, or the catch handled it, or there was nothing to handle), control continues past the construct. If an exception is still in flight (the original was unmatched, or no catch existed), finally runs first and then the exception keeps going.

Every other rule about finally follows from that diagram. The cleanup happens on every path out, which is the central point.

Typical Cleanup Use Cases

The classic places to use finally are around resources whose lifecycle the caller owns: open connections, acquired locks, file handles, audit-log lines. The pattern looks like this for a pretend OrderConnection cleanup hook:

The connection opens before the try. The try body uses it. The finally closes it. Running the same method with a bad order id shows that the cleanup still runs:

The exception is caught and the connection still closes. If the saveOrder call had thrown an unchecked exception that no catch covered, the connection would still close on the way out, before the exception reached the caller.

The same shape works for a cartLock that needs to be released after a critical section:

The lock releases on every call, including the one that throws. Without finally, a thrown exception inside the critical section would leave the lock acquired forever, deadlocking every later caller.

Audit logging follows the same pattern. The "finalise this audit line" step needs to happen whether the work succeeded or failed:

Every order produces an audit line, regardless of outcome. The variable result is set in either the success path or the catch path. The finally writes the line using whichever value is in scope.

A note about resources: Java 7 added try-with-resources, a shorter construct that calls close() automatically on anything that implements AutoCloseable. For closing connections, files, and similar resources, that's the modern replacement for the hand-rolled try / finally pattern shown above. For now, the manual pattern is the one to learn, because it shows what finally is doing internally and applies to cleanup that isn't a close() call (lock release, audit lines, fallback assignment).

Fallback on Parse Failure

Another common use of finally is providing a fallback value when an operation might fail. Consider reading a product price from a string that came from a configuration file:

The finally runs in both cases and confirms which price ended up being used. The fallback assignment lives in the catch, not the finally, because the fallback only applies when parsing failed. The finally is just the cleanup confirmation that runs along every path.

Double.parseDouble throws NumberFormatException on a miss. For input that's malformed frequently (untrusted user input), prefer pre-validating with a regex or a manual scan instead of catching the exception on every call. Throwing fills in a stack trace, which is a real cost in hot loops.

When finally Does NOT Run

The "always runs" property has a precise meaning: it runs whenever the try body completes by any of its normal exits, including throwing. A handful of situations leave the JVM unable to reach the finally block.

The first is System.exit. Calling System.exit(int) requests immediate JVM shutdown. The shutdown sequence does not include running finally blocks of methods that were in progress.

The "this never prints" line is exactly as advertised. Once System.exit is called, the JVM begins shutdown, and pending finally blocks are skipped. (Registered shutdown hooks do run, but they're a different mechanism, not finally.)

The second is Runtime.getRuntime().halt(int). This is more abrupt than System.exit. It stops the JVM without running shutdown hooks or finally blocks. It's rarely used outside emergency-shutdown code.

The third is a hard process kill from outside the JVM. A kill -9 on Linux, an "End Task" on Windows, or any signal that bypasses normal shutdown leaves the JVM no chance to finish anything. The same applies if the process runs out of memory and the OS reaps it, or if the machine loses power.

The fourth is an infinite loop or a blocked thread inside the try body. The finally would run once the body finished, but the body never finishes:

This program prints nothing and runs until killed. The finally is not violating its contract; the contract says "after the try body completes," and the body never completes.

The fifth is a daemon thread when the JVM exits. A daemon thread doesn't keep the JVM alive. If every non-daemon thread finishes while a daemon thread is mid-try, the JVM shuts down and the daemon's finally is skipped. This is the same kind of skip as System.exit: the JVM is going away, and pending blocks don't get to run.

A short table to summarise:

SituationDoes finally run?
Normal completion of tryYes
Exception thrown and caughtYes
Exception thrown and not caughtYes (before propagation)
return inside try or catchYes (before the return takes effect)
System.exit(...) from inside the tryNo
Runtime.getRuntime().halt(...)No
External kill -9 or power lossNo
Infinite loop inside tryNo (body never finishes)
Daemon thread when JVM shuts downNo

The takeaway is that finally is reliable for in-process cleanup, not for catastrophic-failure cleanup. Cleanup that must survive a process crash needs a different mechanism (a write-ahead log, a transaction, an external monitoring system). finally covers the normal cases, not the apocalyptic ones.

The return-in-finally Trap

A return statement inside a finally block overrides whatever was about to happen on the way out, including a return from the try and an in-flight exception. This is a common bug pattern involving finally, and the language allows it for reasons that no longer feel useful.

Here's the construct in its simplest form:

The try evaluates 100 and prepares to return it. Before the return actually takes effect, the finally runs. The finally's return 0 discards the pending return value and returns 0 instead. The caller sees 0. The 100 is gone, with no warning.

The same overriding happens when an exception is in flight:

Both calls return -1. The successful parse of "42" was about to return 42, then finally overrode it with -1. The failing parse of "abc" was about to throw NumberFormatException, then finally overrode it with -1. The exception is swallowed. No stack trace, no log line, no signal to the caller that anything went wrong. The method just returns -1 either way.

What's wrong with this code?

Fix: Don't return from finally. Use finally for cleanup only, and let the try or catch set the return value.

Now the finally does cleanup and nothing else. The return value is whatever the try body computed. For a fallback value on exception, put it in a catch, not a finally. The rule of thumb is short: a finally block should not contain a return. Java doesn't enforce this rule, but every modern style guide and linter does. IntelliJ and Checkstyle both flag a return inside finally as a high-severity warning.

A return inside finally discards both pending return values and pending exceptions, without warning. No exception, no warning, just wrong results. The cost is debugging time, not CPU time.

The throw-in-finally Trap

The same overriding logic applies when a finally throws. If the try was already in the middle of propagating one exception and the finally throws a different one, the original is discarded. Only the new exception reaches the caller. The original problem disappears, often without any record that it ever happened.

The "real problem" is gone. The caller has no signal that validation failed; only that something went wrong in cleanup. Debugging this kind of bug is painful, because the symptom (a complaint about the log file) has nothing to do with the cause (a validation failure earlier in the same method).

This is more common than it sounds. A close call on a flaky connection, an unlock on a lock that's already been released, a write to a log that's been rotated, any of these can throw from inside a finally. If the try was already throwing, the original exception loses.

What's wrong with this code?

The original IllegalArgumentException from saveOrder(null) is gone. The caller only sees the cleanup error, which makes the bug look like a connection problem when it's really an input-validation problem.

Fix: Don't throw from finally. When cleanup can fail, handle that failure inside the finally block itself, typically by logging and moving on:

Now the cleanup failure is logged but doesn't escape. The original validation error reaches the caller, which is what actually matters. The rule of thumb mirrors the return rule: a finally block should not throw. If it must run code that can throw, wrap that code in its own try / catch inside the finally.

When a finally throws while another exception is already in flight, the original exception is discarded. The information loss is the cost. Java 7's try-with-resources keeps the original via Throwable.addSuppressed, which is another reason to prefer it where it applies.

return in try plus a Side Effect in finally

A finally that doesn't return and doesn't throw can still affect what gets returned, but only for object state, not for primitives or references reassigned in finally. The reason is that the return value is captured before finally runs, but the contents of a mutable object are not. This is a source of bugs.

The primitive returns 1 because the value 1 was captured when the return statement evaluated. The reassignment to 999 inside finally doesn't change the already-captured return slot.

The object case returns the same array reference, captured before the finally ran. But the finally mutated the object's contents through the same reference, so the caller sees cart[0] as 999. The reference didn't change; the data behind it did.

The rule that follows: don't rely on finally to influence the return value. When finally reassigns or mutates state that callers depend on, either move the logic into the try or document the side effect carefully. Most of the time, the simplest answer is to leave return-related state alone in finally. Cleanup of resources and logs is fine. Mutation of return values is a hazard.

A Worked Example: Order Processing with Audit and Lock

To pull the pieces together, a method that opens a fake connection, acquires a lock, processes an order, writes an audit line, and releases everything in a finally:

Both orders run the same cleanup. The first succeeds and the audit line records that. The second fails validation, the catch sets result, and the audit line records the failure. The connection closes and the lock releases in both cases. If an unchecked exception not listed in any catch escaped from saveOrder, the cleanup would still run on the way out, and the exception would propagate to main.

This is the textbook use of finally: one block, multiple cleanup steps, runs on every path. It does no return, no throw, no mutation of return-related state.

Why the Order Matters Inside finally

When a finally runs more than one cleanup step, the order matters. A reasonable convention is to release things in the reverse order they were acquired. The audit line happens last, after all resources are released, so the line reflects the final state of the operation.

When any single step in finally could throw, wrap that step in its own try/catch to keep the rest of the cleanup running. The earlier throw-in-finally example showed why: an unhandled throw inside a finally block stops the rest of that block from executing.

The release step throws, the inner catch logs it, the audit line still runs. Without the inner catch, the throw would have skipped the audit step.

Putting It All Together: The Rules

A short list of rules for using finally well, in priority order:

  1. Put cleanup that must run on every path into a finally block.
  2. Don't return from finally. It overrides the try/catch return and swallows exceptions without warning.
  3. Don't throw from finally. It overrides any in-flight exception, losing the original.
  4. If a step inside finally can throw, wrap that step in its own inner try/catch to protect later cleanup.
  5. For closing resources that implement AutoCloseable, prefer try-with-resources. The finally pattern is still correct, just verbose.
  6. Don't rely on finally for catastrophic-failure cleanup. It doesn't run on System.exit, kill -9, or an infinite loop in the try body.
  7. Release resources in reverse acquisition order inside the finally.

The shortest summary: finally runs on every normal exit path from a try. Keep it for cleanup, not for control flow. The moment a finally block starts changing what the method returns or what exception it throws, it stops being cleanup and starts being a bug.

Quiz

finally Block Quiz

10 quizzes