Java finally Block

Su Suriya Ravichandran Updated 14 Nov 2026
9 min read ·Lesson 126 of 133

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
InputWhat happens in the trycatchfinallyAfter the statement
"5"Completes normallySkippedRunsRuns
"five"NumberFormatExceptionRunsRuns after the catchRuns
"0"ArithmeticException, which no catch here matchesSkippedRunsSkipped — 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:

  1. lock.lock() is called before the try, not inside it. If it were inside and lock() itself failed, the finally would call unlock() on a lock this thread never acquired, which throws IllegalMonitorStateException and hides the original problem. Acquire first, then enter try — this is the standard idiom recommended in the Lock documentation.
  2. On the successful withdrawal, the finally releases the lock after the balance changes.
  3. On the failed withdrawal of 9,000, the IllegalArgumentException is thrown. withdraw has no catch, so the finally runs (second "lock released" line), and then the exception reaches main, which prints "rejected". Notice the order in the output: cleanup happens before the caller sees the exception.
  4. 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:

  1. The return expression is evaluated and the value is saved.
  2. The finally block runs.
  3. 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 value 1 is saved when return x executes. Changing the variable x in finally doesn't affect the saved value, so the method returns 1.
  • In builder(), the saved value is a reference to the StringBuilder object. finally doesn'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...catch inside finally, 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 manual finally blocks 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 handle through return; the forbidden one leaves through an exception. The finally block 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 before return and /admin/users would stay at 1.

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.

0 Comments

Reviewed before they appear

No comments yet.

Java
Java Introduction Java Features Java Syntax Java Comments Java Statements Java Output Java Variables Java Data Types Java Type Casting Java Operators Java Strings Java Numbers Java Math Java Booleans Java User Input Java Conditions Java If Else Java Switch Java While Loop Java For Loop Java Break Java Continue Java Arrays Java Methods Java Parameters Java Return Values Java Scope Java Recursion Install Java Install JDK Java IDEs Java JDK Java JRE Java JVM Java Compiler Java File Compile Java Run Java JShell Basics Java Packages Java OOP Java Classes Java Objects Class Methods Constructors Constructor Overload this Keyword static Keyword final Keyword Encapsulation Inheritance Polymorphism Abstraction Access Modifiers super Keyword Method Overload Method Override Instanceof Operator Nested Classes Inner Classes Anonymous Classes Enums Records Array Basics Array Length Loop Arrays Multidimensional Arrays Array Copy Array Sorting Array Search ArrayList LinkedList HashSet TreeSet HashMap TreeMap Queue Interface Deque Interface Stack Class Iterator List Interface Set Interface Map Interface Collections Class Comparable Comparator Generics Basics Generic Classes Generic Methods Wildcards String Basics String Length String Methods String Concatenation String Compare String Search String Replace String Split String Format StringBuilder StringBuffer Text Blocks Regular Expressions Number Types Integer Class Float Class Double Class BigInteger BigDecimal Math Class Random Numbers Number Format Date Basics LocalDate LocalTime LocalDateTime ZonedDateTime Date Formatting Date Parsing Period Class Duration Class Java Instant Class Java Exception Basics Java try...catch Java Multiple Catch Blocks Java finally Block Java throw Keyword Java throws Keyword Java Checked Exceptions Java Unchecked Exceptions Java Custom Exceptions Java try-with-resources Java Exception Chaining
Ask about this post
AI Ask about this post

Ask questions about Java finally Block and get answers drawn from it.

Signed-in readers only.