Question 23 · Section 7

What happens if an exception also occurs in the finally block?

If an exception occurs in finally, it will displace the exception from try. The original error will be lost.

Language versions: English Russian Ukrainian

Junior Level

Main effect

If an exception occurs in finally, it will displace the exception from try. The original error will be lost.

try {
    throw new RuntimeException("Original error");
} finally {
    throw new RuntimeException("Finally error");
}
// Only "Finally error" will be visible

Why it’s dangerous

try {
    // Critical error - DB unavailable
    connection.executeQuery("SELECT * FROM users");
} finally {
    // Error on close - NPE
    connection.close(); // connection == null
}
// Logs only show NPE, information about DB is lost

You’ll be debugging NPE when the real problem is infrastructure.

How to avoid

Use try-with-resources - it automatically handles suppressed exceptions:

try (Connection conn = dataSource.getConnection()) {
    conn.executeQuery("SELECT * FROM users");
}
// If both try and close() fail - both exceptions are preserved

When NOT to rely on finally for cleanup

  1. Code in finally can throw exception - if close(), disconnect(), release() can fail, wrap each call in separate try-catch inside finally
  2. Multiple resources without TWR - if closing 3+ resources in one finally, use separate try-catch for each, otherwise first exception will interrupt closing of the rest
  3. Critical transactions - in finally you can’t safely rollback a transaction if the main exception already occurred; use separate catch block for rollback
  4. Async context - in CompletableFuture finally block may execute in another thread, and exception there will be lost without UncaughtExceptionHandler
  5. Shutdown hooks - on System.exit() finally may not execute at all
  6. Native resource cleanup - for JNI/native memory use Cleaner (Java 9+) instead of finally, as native resources may not free correctly

Middle Level

Suppression mechanism (Shadowing) - step by step

JVM carries only one exception object as “active” in the stack at any moment. Here’s what happens step by step:

Step 1: Code in try throws exception A (new RuntimeException("Original")).

Step 2: JVM marks the current stack frame as “throwing exception” and saves a reference to A in a special slot of the frame.

Step 3: Before exiting the method JVM executes the finally block (this is guaranteed by JLS 14.20.2 specification).

Step 4: Code in finally throws exception B (new RuntimeException("Finally")).

Step 5: JVM replaces the reference in the exception slot of the frame from A to B. Reference to A is lost forever - there’s no way to get it from catch or from outside.

Step 6: Exception B propagates up the stack. A becomes unreachable for GC only after the entire stack is unwound.

Visually:

Stack frame (before finally):  pendingException = A
Stack frame (after finally): pendingException = B  // A is lost!

Concrete code example

public class FinallyShadowing {
    public static void main(String[] args) {
        try {
            System.out.println("Step 1: throw SQLException");
            throw new SQLException("Database connection lost");
        } finally {
            System.out.println("Step 2: finally tries to close resource");
            // close() also fails - and this overshadows SQLException
            throw new NullPointerException("Connection was null in finally");
        }
    }
}

Output in stack trace:

Exception in thread "main" java.lang.NullPointerException: Connection was null in finally
    at FinallyShadowing.main(FinallyShadowing.java:8)

Note: SQLException is completely absent from the stack trace. You’ll be debugging NPE when the real problem is DB connection loss.

How to preserve the original exception:

try {
    throw new SQLException("Database connection lost");
} catch (Exception e) {
    try {
        // finally logic separately
        closeSafely();
    } catch (Exception closeEx) {
        e.addSuppressed(closeEx); // Preserve both
    }
    throw e; // Original exception propagates
}

Shadowing via return

finally can “swallow” an exception even without throwing a new one:

public int dangerousMethod() {
    try {
        throw new RuntimeException("Error");
    } finally {
        return 42; // Exception swallowed! Method will return 42
    }
}

The return instruction in bytecode of finally completes the stack frame before the exception handling mechanism has time to propagate the error. SonarQube marks this as Blocker.

Modern solution: Try-with-resources

Starting from Java 7:

  • If exception in try, then in automatic close() - exception from try remains the main one
  • Exception from close() is added as Suppressed
  • Full picture: Error A (Suppressed: Error B)
try (Resource r = new Resource()) {
    r.doWork(); // IOException #1
} // close() throws IOException #2

// #1 - main, #2 - suppressed
catch (Exception e) {
    System.out.println(e.getMessage());         // #1
    System.out.println(e.getSuppressed()[0]);   // #2
}

Senior Level

Resource Leaks on exception in finally

If exception in finally occurred in the middle of cleanup (closing first of five sockets), the remaining resources will not be closed - execution of finally will be interrupted.

Senior Best Practice: wrap each close() in a separate try-catch inside finally:

finally {
    try { resource1.close(); } catch (Exception e) { log.warn("Failed to close 1", e); }
    try { resource2.close(); } catch (Exception e) { log.warn("Failed to close 2", e); }
}

Or use try-with-resources - it does this automatically.

Analyzing Logs

If you see strange exceptions from cleanup blocks in logs (SocketClosedException), always check if they’re hiding earlier errors.

Debugger

When step debugging, watch the transition to finally. If the exception variable in the stack frame suddenly changes - you caught Shadowing.

Bytecode analysis

javap -c MyClass

Will show how return in finally overwrites the return value and how exception overwrites the previous one.

Diagnostics

  • getSuppressed() - check the suppressed exceptions array
  • Static Analysis - Sonar warns about risky finally blocks
  • Log Correlation - link exceptions from try and finally by Trace ID

Interview Cheat Sheet

Must know:

  • Exception from finally displaces exception from try - original error lost forever
  • JVM stores only one pending exception in the frame slot - finally overwrites it
  • return in finally also “swallows” exception - method will return value instead of error
  • Try-with-resources automatically preserves both exceptions: main + suppressed from close()
  • Without TWR wrap each close() in separate try-catch inside finally
  • On multiple resources in finally, first exception will interrupt closing of the rest - resource leak
  • In async context, exception in finally may be lost without UncaughtExceptionHandler

Frequent follow-up questions:

  • How to see the original exception? - Without TWR, no way - it’s lost. Use try-with-resources
  • Why is TWR better than finally? - Preserves both exceptions via suppressed mechanism
  • What if finally does return? - Exception from try is swallowed, method returns value
  • How to properly close resources without TWR? - Each close() in separate try-catch inside finally

Red flags (NOT to say):

  • “It’s safe to throw exceptions in finally” - they will overshadow the original error
  • “I use finally for closing resources” - outdated approach, use TWR
  • “Return in finally is normal practice” - SonarQube marks as Blocker
  • “Lost exception is not a big deal” - you’ll be debugging the wrong problem

Related topics:

  • [[23. What are suppressed exceptions]] - how TWR preserves both exceptions
  • [[9. What is try-with-resources]] - automatic resource management
  • [[8. Is the execution of a finally block guaranteed]] - when finally may not execute
  • [[19. Why you should not swallow exceptions (catch empty)]] - error loss