Java Tutorial
🔍

Java finally Block

Java finally Block

The finally block runs after a try block finishes — no matter how it finishes. Exception thrown, return executed, normal completion — the finally block runs in all three cases. This guarantee exists for one purpose: cleanup that must always happen regardless of what the code inside try does. Connection closed, lock released, audit entry written — the finally block is where code goes when "I'll handle it later if something goes wrong" is not an acceptable plan.

What Is the finally Block?

finally is an optional clause that follows a try or try-catch block. It marks code that the JVM commits to executing before control leaves the try-catch-finally structure — whether the try completed normally, threw an exception that was caught, threw an exception that was not caught, or executed a return statement.

EXECUTION GUARANTEE:

  Normal execution:
    try body runs fully → finally runs → execution continues after structure

  Exception caught:
    try body throws → catch block runs → finally runs → execution continues

  Exception not caught:
    try body throws → no matching catch → finally runs → exception propagates

  return in try:
    try body executes return → finally runs → THEN the return takes effect

  return in catch:
    catch block executes return → finally runs → THEN the return takes effect

WHEN finally DOES NOT RUN:
  System.exit() is called inside try or catch — JVM process terminates
  JVM itself crashes (OutOfMemoryError that kills the process, power failure)
  The thread is killed abruptly via Thread.stop() (deprecated, still exists)

Basic Overview — All finally Behaviour

FORMS:
  try { } finally { }                     ← no catch — exception propagates after finally
  try { } catch (E e) { } finally { }     ← full form — most common

WHAT finally IS USED FOR:
  1. Closing resources that do NOT implement AutoCloseable
     — ReentrantLock, custom handles, legacy objects
  2. Recording metrics and audit log entries (must always record)
  3. Releasing permits (Semaphore) regardless of outcome
  4. Resetting state that must be restored after each operation
  5. Logging the end of a request/transaction for traceability

WHAT finally REPLACED BY try-with-resources:
  Any resource implementing AutoCloseable (Closeable, Connection,
  InputStream, OutputStream, PreparedStatement, etc.) should use
  try-with-resources — it is cleaner and handles exception suppression correctly.

CRITICAL EDGE CASES (all appear in interviews):
  return inside try    → finally runs, THEN return takes effect
  return inside finally → OVERRIDES any return or exception from try/catch
  throw inside finally → SUPPRESSES the original exception from try/catch
  System.exit(0)       → finally SKIPPED — JVM terminates immediately

GUARANTEED ORDER:
  try body (partially or fully) → catch (if applicable) → finally → (return or rethrow)

How finally Works Internally

The JVM implements finally using a technique called inline duplication of the finally body. The compiler generates a copy of the finally block for every possible exit path from the try-catch structure.

COMPILER-GENERATED BYTECODE STRUCTURE:

  Source:
    try {
        operation();         // may throw or return
    } catch (Exception e) {
        handleFailure(e);    // may throw or return
    } finally {
        cleanup();           // always needed
    }

  Bytecode (conceptually):
    try path:
        operation()          ← runs
        cleanup()            ← finally inlined here (normal exit)
        continue

    catch path:
        handleFailure(e)     ← runs if exception caught
        cleanup()            ← finally inlined here (catch exit)
        continue

    exception-not-caught path:
        cleanup()            ← finally inlined here (exception exit)
        rethrow              ← original exception propagates

  The finally body appears multiple times in bytecode — once per exit path.
  This is why System.exit() bypasses finally: it terminates the JVM
  before any further bytecode executes, skipping the inlined copies.

RETURN INTERACTION:
  When return executes inside try, the JVM saves the return value,
  runs finally, then delivers the saved return value to the caller.
  If finally also has a return, it REPLACES the saved value.
  This is the most common finally-related interview trap.

Syntax and Usage

Basic finally — Cleanup That Always Happens

1// File: FinallyBasicsDemo.java 2 3public class FinallyBasicsDemo { 4 5 // Simulates a resource that needs cleanup after each use 6 static class AuditLogger { 7 private final String operationId; 8 private boolean closed = false; 9 10 AuditLogger(String operationId) { 11 this.operationId = operationId; 12 System.out.println(" [AUDIT] Start: " + operationId); 13 } 14 15 void logSuccess(String detail) { 16 if (closed) throw new IllegalStateException("Logger already closed"); 17 System.out.println(" [AUDIT] Success: " + detail); 18 } 19 20 void logFailure(String reason) { 21 if (closed) throw new IllegalStateException("Logger already closed"); 22 System.out.println(" [AUDIT] Failure: " + reason); 23 } 24 25 void close() { 26 closed = true; 27 System.out.println(" [AUDIT] Closed: " + operationId); 28 } 29 } 30 31 static void processPayment(String orderId, boolean shouldFail) { 32 AuditLogger audit = new AuditLogger("PAY-" + orderId); 33 System.out.println("Processing payment for order: " + orderId); 34 try { 35 if (shouldFail) { 36 throw new RuntimeException("Payment gateway timeout"); 37 } 38 audit.logSuccess("Payment confirmed"); 39 System.out.println("Payment succeeded"); 40 41 } catch (RuntimeException exception) { 42 audit.logFailure(exception.getMessage()); 43 System.out.println("Payment failed: " + exception.getMessage()); 44 45 } finally { 46 // Audit logger MUST be closed regardless of success or failure 47 // Not AutoCloseable — cannot use try-with-resources here without modification 48 audit.close(); 49 } 50 } 51 52 public static void main(String[] args) { 53 System.out.println("=== Successful payment ==="); 54 processPayment("ORD-001", false); 55 56 System.out.println(); 57 58 System.out.println("=== Failed payment ==="); 59 processPayment("ORD-002", true); 60 } 61}
Output:
=== Successful payment ===
  [AUDIT] Start: PAY-ORD-001
Processing payment for order: ORD-001
  [AUDIT] Success: Payment confirmed
Payment succeeded
  [AUDIT] Closed: PAY-ORD-001

=== Failed payment ===
  [AUDIT] Start: PAY-ORD-002
Processing payment for order: ORD-002
  [AUDIT] Failure: Payment gateway timeout
Payment failed: Payment gateway timeout
  [AUDIT] Closed: PAY-ORD-002

finally With return — The Override Trap

The most common finally interview question: when return appears in both try and finally, the finally return wins. The JVM saves the return value from the try block, runs finally, and if finally has its own return, it discards the saved value and uses the new one.

1// File: FinallyReturnDemo.java 2 3public class FinallyReturnDemo { 4 5 // TRAP: finally has a return — it overrides the try return 6 static String returnsFromTry() { 7 try { 8 System.out.println(" try: about to return 'try-value'"); 9 return "try-value"; // JVM saves "try-value", then runs finally 10 } finally { 11 System.out.println(" finally: running after try's return"); 12 // No return here — try's saved "try-value" is delivered 13 } 14 } 15 16 // TRAP: finally return OVERRIDES try return — dangerous pattern 17 static String finallyOverridesReturn() { 18 try { 19 System.out.println(" try: returning 'try-value'"); 20 return "try-value"; // JVM saves "try-value" 21 } finally { 22 System.out.println(" finally: returning 'finally-value' — OVERRIDES try"); 23 return "finally-value"; // Replaces saved "try-value" 24 } 25 } 26 27 // TRAP: finally return SUPPRESSES exception from try 28 static String finallyReturnSuppressesException() { 29 try { 30 System.out.println(" try: throwing RuntimeException"); 31 throw new RuntimeException("exception from try"); 32 } finally { 33 System.out.println(" finally: returning — SUPPRESSES the exception!"); 34 return "finally-value"; // The RuntimeException is completely lost 35 } 36 } 37 38 // ALSO: catch return is also overridden by finally return 39 static String catchReturnOverridden() { 40 try { 41 throw new RuntimeException("trigger catch"); 42 } catch (RuntimeException exception) { 43 System.out.println(" catch: returning 'catch-value'"); 44 return "catch-value"; // JVM saves "catch-value" 45 } finally { 46 System.out.println(" finally: returning — overrides catch return"); 47 return "finally-value"; // Replaces saved "catch-value" 48 } 49 } 50 51 public static void main(String[] args) { 52 53 System.out.println("=== finally without return — try value delivered ==="); 54 System.out.println("Result: " + returnsFromTry()); 55 56 System.out.println(); 57 58 System.out.println("=== finally WITH return — overrides try return ==="); 59 System.out.println("Result: " + finallyOverridesReturn()); 60 61 System.out.println(); 62 63 System.out.println("=== finally return SUPPRESSES exception ==="); 64 System.out.println("Result: " + finallyReturnSuppressesException()); 65 System.out.println("(The RuntimeException was completely swallowed)"); 66 67 System.out.println(); 68 69 System.out.println("=== finally overrides catch return ==="); 70 System.out.println("Result: " + catchReturnOverridden()); 71 } 72}
Output:
=== finally without return — try value delivered ===
  try: about to return 'try-value'
  finally: running after try's return
Result: try-value

=== finally WITH return — overrides try return ===
  try: returning 'try-value'
  finally: returning 'finally-value' — OVERRIDES try
Result: finally-value

=== finally return SUPPRESSES exception ===
  try: throwing RuntimeException
  finally: returning — SUPPRESSES the exception!
Result: finally-value
(The RuntimeException was completely swallowed)

=== finally overrides catch return ===
  catch: returning 'catch-value'
  finally: returning — overrides catch return
Result: finally-value

finally and Exception Suppression

When finally throws an exception, the original exception from the try block is suppressed — it disappears unless explicitly handled.

1// File: FinallyExceptionDemo.java 2 3public class FinallyExceptionDemo { 4 5 // DANGER: finally throws — original exception from try is suppressed 6 static void suppressesOriginalException() throws Exception { 7 try { 8 System.out.println(" try: throwing original exception"); 9 throw new Exception("ORIGINAL exception from try"); 10 } finally { 11 System.out.println(" finally: throwing — SUPPRESSES original"); 12 throw new Exception("Exception from finally"); // original is lost 13 } 14 } 15 16 // BETTER: manually preserve original exception by adding it as suppressed 17 static void preservesOriginalViaSuppressed() throws Exception { 18 Exception original = null; 19 try { 20 System.out.println(" try: throwing original exception"); 21 throw new Exception("ORIGINAL exception from try"); 22 } catch (Exception tryException) { 23 original = tryException; // save the original 24 throw tryException; // rethrow to continue propagation 25 } finally { 26 if (original != null) { 27 try { 28 System.out.println(" finally: performing risky cleanup"); 29 throw new Exception("Cleanup failed"); 30 } catch (Exception cleanupException) { 31 // Attach cleanup failure as a suppressed exception 32 // Java 7+ Throwable.addSuppressed() — used by try-with-resources 33 original.addSuppressed(cleanupException); 34 System.out.println(" Cleanup failure attached as suppressed"); 35 } 36 } 37 } 38 } 39 40 // This is exactly what try-with-resources does automatically for AutoCloseable 41 static void tryCatchFinallyWithSuppressedCheck() { 42 System.out.println("--- Exception suppression in finally ---"); 43 try { 44 suppressesOriginalException(); 45 } catch (Exception exception) { 46 System.out.println("Caught: " + exception.getMessage()); 47 System.out.println("Suppressed count: " + exception.getSuppressed().length); 48 // The ORIGINAL exception from try is gone — only finally's exception survives 49 } 50 } 51 52 public static void main(String[] args) { 53 54 System.out.println("=== finally exception SUPPRESSES original ==="); 55 tryCatchFinallyWithSuppressedCheck(); 56 57 System.out.println(); 58 59 System.out.println("=== Manually preserved via addSuppressed ==="); 60 try { 61 preservesOriginalViaSuppressed(); 62 } catch (Exception exception) { 63 System.out.println("Caught: " + exception.getMessage()); 64 System.out.println("Suppressed count: " + exception.getSuppressed().length); 65 for (Throwable suppressed : exception.getSuppressed()) { 66 System.out.println(" Suppressed: " + suppressed.getMessage()); 67 } 68 } 69 } 70}
Output:
=== finally exception SUPPRESSES original ===
  try: throwing original exception
  finally: throwing — SUPPRESSES original
Caught: Exception from finally
Suppressed count: 0

=== Manually preserved via addSuppressed ===
  try: throwing original exception
  finally: performing risky cleanup
  Cleanup failure attached as suppressed
Caught: ORIGINAL exception from try
Suppressed count: 1
  Suppressed: Cleanup failed

finally vs try-with-resources

For any resource that implements AutoCloseable, try-with-resources is strictly better than try-finally. It handles exception suppression correctly (as shown above) and eliminates the possibility of forgetting to close.

1// File: FinallyVsTryWithResourcesDemo.java 2 3import java.io.Closeable; 4import java.io.IOException; 5 6public class FinallyVsTryWithResourcesDemo { 7 8 static class DatabaseConnection implements Closeable { 9 private final String url; 10 private boolean open = true; 11 12 DatabaseConnection(String url) throws IOException { 13 this.url = url; 14 System.out.println(" Connection opened: " + url); 15 } 16 17 String query(String sql) throws IOException { 18 if (!open) throw new IOException("Connection is closed"); 19 if (sql.contains("FAIL")) throw new IOException("Query failed: " + sql); 20 return "result-of-[" + sql + "]"; 21 } 22 23 @Override 24 public void close() throws IOException { 25 open = false; 26 System.out.println(" Connection closed: " + url); 27 } 28 } 29 30 // LEGACY: try-finally for resource management 31 static void withTryFinally(String sql) throws IOException { 32 System.out.println("-- try-finally approach --"); 33 DatabaseConnection conn = new DatabaseConnection("jdbc:pg://localhost/orders"); 34 try { 35 String result = conn.query(sql); 36 System.out.println(" Query result: " + result); 37 } finally { 38 conn.close(); // must remember to call — compile does not enforce it 39 } 40 } 41 42 // MODERN: try-with-resources — close() called automatically 43 static void withTryWithResources(String sql) throws IOException { 44 System.out.println("-- try-with-resources approach --"); 45 try (DatabaseConnection conn = 46 new DatabaseConnection("jdbc:pg://localhost/orders")) { 47 String result = conn.query(sql); 48 System.out.println(" Query result: " + result); 49 } // conn.close() called automatically here — even if query() throws 50 } 51 52 public static void main(String[] args) { 53 54 System.out.println("=== Successful query — both approaches ==="); 55 try { 56 withTryFinally("SELECT * FROM orders LIMIT 10"); 57 } catch (IOException e) { 58 System.out.println(" Error: " + e.getMessage()); 59 } 60 61 System.out.println(); 62 63 try { 64 withTryWithResources("SELECT * FROM orders LIMIT 10"); 65 } catch (IOException e) { 66 System.out.println(" Error: " + e.getMessage()); 67 } 68 69 System.out.println(); 70 71 System.out.println("=== Failed query — both close correctly ==="); 72 try { 73 withTryFinally("SELECT * FROM FAIL_TABLE"); 74 } catch (IOException e) { 75 System.out.println(" Caller caught: " + e.getMessage()); 76 } 77 78 System.out.println(); 79 80 try { 81 withTryWithResources("SELECT * FROM FAIL_TABLE"); 82 } catch (IOException e) { 83 System.out.println(" Caller caught: " + e.getMessage()); 84 } 85 } 86}
Output:
=== Successful query — both approaches ===
-- try-finally approach --
  Connection opened: jdbc:pg://localhost/orders
  Query result: result-of-[SELECT * FROM orders LIMIT 10]
  Connection closed: jdbc:pg://localhost/orders

-- try-with-resources approach --
  Connection opened: jdbc:pg://localhost/orders
  Query result: result-of-[SELECT * FROM orders LIMIT 10]
  Connection closed: jdbc:pg://localhost/orders

=== Failed query — both close correctly ===
-- try-finally approach --
  Connection opened: jdbc:pg://localhost/orders
  Connection closed: jdbc:pg://localhost/orders
  Caller caught: Query failed: SELECT * FROM FAIL_TABLE

-- try-with-resources approach --
  Connection opened: jdbc:pg://localhost/orders
  Connection closed: jdbc:pg://localhost/orders
  Caller caught: Query failed: SELECT * FROM FAIL_TABLE

Real-World Example — Zepto Inventory Lock Service

An inventory reservation service at Zepto acquires a ReentrantLock before modifying stock. Locks do not implement AutoCloseabletry-with-resources cannot be used here. finally is the only reliable way to guarantee the lock is released even if the reservation logic throws midway. A missed lock.unlock() would leave the lock permanently held, blocking every subsequent request for that inventory item.

1// File: InventoryLockService.java 2 3import java.util.HashMap; 4import java.util.Map; 5import java.util.concurrent.locks.ReentrantLock; 6 7public class InventoryLockService { 8 9 private final Map<String, Integer> stock = new HashMap<>(); 10 private final Map<String, ReentrantLock> locks = new HashMap<>(); 11 12 public InventoryLockService() { 13 stock.put("P001", 10); stock.put("P002", 3); 14 stock.put("P003", 0); 15 locks.put("P001", new ReentrantLock()); 16 locks.put("P002", new ReentrantLock()); 17 locks.put("P003", new ReentrantLock()); 18 } 19 20 // ReentrantLock does NOT implement AutoCloseable — must use try-finally 21 public boolean reserveStock(String productId, int quantity, String orderId) { 22 ReentrantLock lock = locks.get(productId); 23 if (lock == null) { 24 throw new IllegalArgumentException("Unknown product: " + productId); 25 } 26 27 System.out.printf(" [%s] Acquiring lock for %s...%n", orderId, productId); 28 lock.lock(); // acquire the lock — MUST be released in finally 29 try { 30 // All mutation happens inside the lock — safe from concurrent modification 31 int available = stock.getOrDefault(productId, 0); 32 System.out.printf(" [%s] Locked %s — available: %d, requested: %d%n", 33 orderId, productId, available, quantity); 34 35 if (available < quantity) { 36 // Business condition — throws, but finally still releases the lock 37 throw new IllegalStateException( 38 String.format("Insufficient stock for %s: need %d, have %d", 39 productId, quantity, available)); 40 } 41 42 if (productId.equals("P002") && quantity > 1) { 43 // Simulate an unexpected system failure mid-reservation 44 throw new RuntimeException( 45 "Inventory DB write failed for product: " + productId); 46 } 47 48 stock.put(productId, available - quantity); 49 System.out.printf(" [%s] Reserved %d of %s. Remaining: %d%n", 50 orderId, quantity, productId, stock.get(productId)); 51 return true; 52 53 } finally { 54 // ALWAYS release the lock — no matter what happened above 55 // Without this finally block, a thrown exception would leave 56 // the lock permanently held — next caller would block forever 57 lock.unlock(); 58 System.out.printf(" [%s] Lock released for %s%n", orderId, productId); 59 } 60 } 61 62 public void printStock() { 63 System.out.println("Current stock: " + stock); 64 } 65 66 public static void main(String[] args) { 67 InventoryLockService service = new InventoryLockService(); 68 69 System.out.println("=== Successful reservation ==="); 70 try { 71 service.reserveStock("P001", 3, "ORD-001"); 72 } catch (Exception e) { 73 System.out.println(" Failed: " + e.getMessage()); 74 } 75 service.printStock(); 76 77 System.out.println(); 78 79 System.out.println("=== Insufficient stock — lock still released ==="); 80 try { 81 service.reserveStock("P003", 2, "ORD-002"); 82 } catch (IllegalStateException ise) { 83 System.out.println(" Business error: " + ise.getMessage()); 84 } 85 86 System.out.println(); 87 88 System.out.println("=== System failure — lock still released ==="); 89 try { 90 service.reserveStock("P002", 2, "ORD-003"); 91 } catch (RuntimeException rte) { 92 System.out.println(" System error: " + rte.getMessage()); 93 } 94 95 System.out.println(); 96 97 System.out.println("=== Verify P001 is still accessible after releases ==="); 98 try { 99 service.reserveStock("P001", 2, "ORD-004"); 100 } catch (Exception e) { 101 System.out.println(" Failed: " + e.getMessage()); 102 } 103 service.printStock(); 104 } 105}
Output:
=== Successful reservation ===
  [ORD-001] Acquiring lock for P001...
  [ORD-001] Locked P001 — available: 10, requested: 3
  [ORD-001] Reserved 3 of P001. Remaining: 7
  [ORD-001] Lock released for P001
Current stock: {P001=7, P002=3, P003=0}

=== Insufficient stock — lock still released ===
  [ORD-002] Acquiring lock for P003...
  [ORD-002] Locked P003 — available: 0, requested: 2
  [ORD-002] Lock released for P003
  Business error: Insufficient stock for P003: need 2, have 0

=== System failure — lock still released ===
  [ORD-003] Acquiring lock for P002...
  [ORD-003] Locked P002 — available: 3, requested: 2
  [ORD-003] Lock released for P002
  System error: Inventory DB write failed for product: P002

=== Verify P001 is still accessible after releases ===
  [ORD-004] Acquiring lock for P001...
  [ORD-004] Locked P001 — available: 7, requested: 2
  [ORD-004] Reserved 2 of P001. Remaining: 5
  [ORD-004] Lock released for P001
Current stock: {P001=5, P002=3, P003=0}

Performance Considerations

The finally block has no measurable overhead in the happy path. The JVM's exception table approach means there is no runtime check when the try body executes normally — the finally code simply runs as part of the normal execution path after the try block completes.

FINALLY COST:

  try block execution (no exception): no overhead — no table lookup needed
  finally block execution: runs once as normal code — no extra indirection
  Exception path: exception table consulted → finally code runs

  WHAT IS NOT FREE:
  — Heavy computation or I/O inside finally (same cost as anywhere else)
  — Creating objects inside finally (allocation cost applies)
  — Throwing exceptions inside finally (stack trace capture is expensive)

  RULE OF THUMB:
  finally should contain lightweight cleanup: close(), unlock(), counter increment
  Avoid long-running operations, database calls, or complex logic in finally
  The block is not time-bounded — a slow finally delays callers

  try-with-resources vs try-finally:
  Identical runtime performance for the happy path.
  try-with-resources is preferred for AutoCloseable resources:
    — correctness advantage (exception suppression handled)
    — no risk of forgetting the close() call

Best Practices

Put only cleanup code in finally, nothing that can fail in a way that matters. A finally block that throws suppresses the original exception and changes the method's observable behaviour. Calls inside finally should be as unconditional and failure-resistant as possible — lock.unlock(), counter.decrement(), logger.close(). If the cleanup can fail in a meaningful way, wrap it in its own try-catch inside finally and add the failure as a suppressed exception on the original.

Use try-with-resources for everything that implements AutoCloseable. Every JDK I/O class, JDBC connection, statement, and result set implements Closeable (which extends AutoCloseable). try-with-resources handles the close call automatically, handles exception suppression correctly, and is immune to the "forgot the close" bug. Reserve manual finally for resources that do not implement AutoCloseable — primarily java.util.concurrent.locks.Lock and similar.

Never put a return statement inside finally. A return in finally silently discards any exception from try or catch, making the method appear to succeed even when it failed. It also discards any return value computed in try or catch. IDEs flag this as a code smell; code reviews should treat it as a defect. The only place finally should transfer control is through throw — and even that should be done carefully to avoid suppression.

Release locks in finally only after acquiring them. A common mistake is placing lock.unlock() in finally before lock.lock() has been called — if lock() itself throws (rare but possible), the finally block will call unlock() on an unacquired lock, throwing IllegalMonitorStateException. The pattern: acquire the lock immediately before the try block with no code between lock.lock() and try.

Common Mistakes

Mistake 1 — return Inside finally Silently Discards Exceptions

1// WRONG — return in finally discards the RuntimeException from try 2// Callers see a successful return of 0 when the operation actually failed 3static int dangerousMethod() { 4 try { 5 throw new RuntimeException("Critical failure in try block"); 6 } finally { 7 return 0; // exception is completely swallowed — not caught, not rethrown 8 } 9} 10 11// Caller has no idea this method failed: 12int result = dangerousMethod(); // result = 0, no exception — silent failure 13 14// CORRECT — no return in finally; let exception propagate 15static int safeMethod() { 16 try { 17 throw new RuntimeException("Critical failure"); 18 } finally { 19 // Cleanup only — no return, no throw 20 System.out.println("Cleanup completed"); 21 } 22 // RuntimeException propagates to caller — they know something failed 23}

Mistake 2 — Calling unlock() Before lock() in finally

1java.util.concurrent.locks.ReentrantLock lock = new java.util.concurrent.locks.ReentrantLock(); 2 3// WRONG — lock.unlock() in finally runs even if lock.lock() was never called 4// If lock.lock() throws, the finally will call unlock() on an unheld lock 5try { 6 lock.lock(); // if this throws for any reason... 7 operation(); 8} finally { 9 lock.unlock(); // ...this throws IllegalMonitorStateException 10} 11 12// CORRECT — lock before try, unlock in finally 13lock.lock(); // acquire lock OUTSIDE the try block 14try { 15 operation(); // everything after lock acquisition is inside try 16} finally { 17 lock.unlock(); // safe — we know lock was acquired before entering try 18}

Mistake 3 — Throwing Inside finally Suppresses the Original Exception

1// WRONG — exception from finally replaces original exception from try 2// Callers see "Connection close failed" when the real problem was in saveOrder() 3static void saveOrder(Order order) throws Exception { 4 try { 5 database.save(order); // throws Exception("Validation failed") 6 } finally { 7 // If this also throws, the "Validation failed" exception is lost 8 connection.close(); // throws Exception("Connection close failed") 9 } 10} 11 12// CORRECT — wrap cleanup in its own try-catch inside finally 13static void saveOrderSafe(Order order) throws Exception { 14 try { 15 database.save(order); 16 } finally { 17 try { 18 connection.close(); 19 } catch (Exception closeException) { 20 // Log the close failure but do not let it suppress the original 21 System.err.println("Connection close failed: " + closeException.getMessage()); 22 } 23 } 24}

Mistake 4 — Assuming finally Runs When System.exit() Is Called

1// WRONG assumption — System.exit() terminates the JVM immediately 2// finally DOES NOT run after System.exit() 3static void configLoader() { 4 try { 5 if (config == null) { 6 System.exit(1); // JVM terminates — finally is SKIPPED 7 } 8 } finally { 9 System.out.println("Config cleanup"); // NEVER PRINTED after exit(1) 10 } 11} 12 13// CORRECT — use a shutdown hook for cleanup that must run before JVM exits 14Runtime.getRuntime().addShutdownHook(new Thread(() -> { 15 System.out.println("Shutdown hook: releasing resources"); 16 // This runs when JVM exits via System.exit(), CTRL+C, or normal completion 17}));

Interview Questions

Q1. When does the finally block execute in Java?

finally executes after the try block completes — regardless of how it completes. If the try body finishes normally, finally runs before execution continues. If the try body throws and the exception is caught, finally runs after the catch block. If the try body throws and no catch matches, finally runs before the exception propagates to the caller. If a return statement is executed inside try or catch, finally runs before the return value is delivered. The only cases where finally does not execute: System.exit() is called, the JVM crashes, or the thread is forcibly stopped.

Q2. What happens when a return statement is inside both try and finally?

When return executes inside try, the JVM saves the return value, runs the finally block, and then delivers the saved value to the caller. If finally also contains a return statement, it executes a second return — this second return replaces the saved value from try. The caller receives the finally return value, and the try return value is silently discarded. This is one of the most dangerous patterns in Java — it makes the method's observable behaviour dependent on whether an exception occurred, and it silently swallows exceptions from try when finally returns a value.

Q3. What happens when both try and finally throw exceptions?

When try throws and finally also throws, the original exception from try is suppressed — it is permanently lost unless explicitly preserved. The caller only receives the exception from finally. This is why the finally block should never throw unchecked exceptions and any AutoCloseable.close() calls that might throw should be wrapped in their own try-catch. Java 7's try-with-resources handles this correctly: if both try and the auto-close throw, the auto-close exception is attached as a suppressed exception (Throwable.addSuppressed()), not silently discarded.

Q4. Why is try-with-resources preferred over try-finally for resources?

try-with-resources offers three advantages over manual try-finally. First, close() is called automatically — the compiler generates the call, eliminating the risk of forgetting it. Second, exception suppression is handled correctly — if both the try body and close() throw, close()'s exception is attached via addSuppressed() to the primary exception, not replacing it. Third, multiple resources are closed in reverse order of declaration, and each close is independent — one failed close does not prevent subsequent closes. Manual try-finally requires careful code to achieve the same correctness.

Q5. Can a try block exist without a catch block if finally is present?

Yes. try { } finally { } without any catch block is valid Java. In this form, exceptions from the try block propagate to the caller after finally runs — there is no handling inside the method. This pattern is appropriate when the method cannot handle the exception but must run cleanup before propagation: lock.lock(); try { operation(); } finally { lock.unlock(); } is the canonical example. The exception propagates unchanged; the lock is guaranteed to be released.

Q6. What is the correct pattern for using ReentrantLock with finally?

Acquire the lock immediately before the try block with no code between lock.lock() and try {. Place lock.unlock() in the finally block. This guarantees two things: the lock is released even if the code inside try throws, and unlock() is only called when the lock was actually acquired (avoiding IllegalMonitorStateException if lock() itself threw). The pattern is: lock.lock(); try { ... } finally { lock.unlock(); } — no code between lock() and try.

FAQs

Does finally run when an OutOfMemoryError is thrown?

It depends. If the OutOfMemoryError (OOME) is thrown inside the try block, finally may or may not run depending on the JVM's state. In practice, if the heap is exhausted, the JVM may not be able to execute the finally body if it requires any heap allocation. For graceful shutdown in the event of OOME, a JVM shutdown hook (Runtime.getRuntime().addShutdownHook(...)) is more reliable than finally.

Can finally be used without any exception occurring?

Yes — finally runs on every exit from the try-catch structure, including normal completion. If the try body executes without throwing, finally still runs before execution continues after the structure. This is commonly used for metrics recording — the counter should increment whether the operation succeeded or failed.

What is the difference between finally and a shutdown hook?

finally runs when control leaves a specific try-catch-finally structure — it is scoped to one method invocation. A shutdown hook (Runtime.getRuntime().addShutdownHook(new Thread(...))) runs when the JVM exits — whether through System.exit(), a normal main method return, or an unhandled exception that terminates the last non-daemon thread. Shutdown hooks are for process-level cleanup; finally is for method-level cleanup.

Is it possible for finally to execute twice for the same try block?

No. For any single execution of a try block, finally executes exactly once — when that execution of try exits. If the method is called multiple times, each call has its own try-finally execution. Recursive calls each have their own stack frame and their own finally execution.

Does finally run after a StackOverflowError?

Yes, usually. StackOverflowError is thrown on the thread whose call stack overflowed. If the finally block is reachable with the remaining stack space (which is small at that point), it executes. If the finally block itself requires additional stack frames that trigger another StackOverflowError, the cleanup may not complete. In practice, finally blocks should be very lightweight — no deep call chains — to ensure they can execute even with a damaged stack.

How does try-with-resources handle the finally behaviour internally?

try-with-resources generates bytecode equivalent to a try-finally where the finally calls close() on each declared resource in reverse order of declaration. The key improvement over manual try-finally: if both the try body and close() throw, the close() exception is attached to the primary exception using Throwable.addSuppressed() — not discarded. Callers retrieve suppressed exceptions with exception.getSuppressed(). Manual try-finally requires explicit addSuppressed() calls to achieve the same correctness.

Summary

finally is the guarantee that certain code always runs when a try block exits. Its execution contract is unconditional with two narrow exceptions: System.exit() bypasses it entirely, and a JVM crash prevents execution. For all other exit paths — normal completion, caught exception, uncaught exception, return statement — finally executes.

Three traps govern production-quality finally usage. A return inside finally overrides any return or exception from try and catch — never put return in finally. A throw inside finally suppresses the original exception — wrap cleanup calls in their own try-catch if they might throw. Calling unlock() in finally before the corresponding lock() causes IllegalMonitorStateException — always acquire locks before entering try.

For resources that implement AutoCloseable, try-with-resources handles all of this correctly and should always be preferred. finally remains irreplaceable for non-AutoCloseable cleanup: ReentrantLock, metrics recording, audit log entries, and Semaphore permit release — anything that must always happen and cannot be automated by the compiler's AutoCloseable mechanism.

What to Read Next