Back to try-catch-finally
Q2Question 2 of 4

Why is throw/return in finally dangerous?

Key Concepts
  • If try throws ExceptionA and finally throws ExceptionB, ExceptionA is silently discarded and only ExceptionB reaches the caller
  • If try returns a value and finally also returns, the finally return overrides the try return silently
  • If try throws an exception and finally returns a value, the exception is completely swallowed and the method appears to succeed
  • The original exception is not suppressed or attached as a cause but completely gone from memory
  • try-with-resources solves this by making the try exception primary and the close exception suppressed via addSuppressed()
  • SonarQube rule squid:S1143 flags return and throw in finally blocks as critical issues

Definition

A throw or return in finally silently discards the try block's exception or return value, causing the real error to vanish completely and making debugging impossible.
  • If try throws ExceptionA and finally throws ExceptionB, ExceptionA is silently discarded. Not suppressed, not attached as a cause, completely gone. The caller only sees ExceptionB.
  • If try returns value X and finally returns value Y, the caller receives Y. Value X is silently overridden.
  • If try throws an exception and finally returns a value, the exception is swallowed entirely. The method appears to succeed when it actually failed.
  • try-with-resources fixes this: the try body's exception becomes the primary (propagated) and close()'s exception becomes suppressed (attached via addSuppressed(), visible in stack traces).
  • Production rule: never throw or return from finally.

The bank vault catches fire (try throws "FIRE"). The security guard tries to lock the door (finally) but drops the key (finally throws "KEY DROPPED"). The incident report only says "KEY DROPPED." The fire is never reported. Everyone investigates the dropped key while the vault burns. That is what throw in finally does: it replaces the real emergency with a trivial cleanup problem.

Understanding the Concepts

What happens when finally throws an exception?

If the try block throws an exception and the finally block also throws, the try block's exception is silently lost. The caller only sees the finally block's exception.

JAVA
package com.bank.demo;

public class FinallyThrowDemo {

    public static void dangerousMethod() {
        try {
            throw new RuntimeException("DATABASE CORRUPTION");
        } finally {
            throw new RuntimeException("Failed to close connection");
        }
    }

    public static void main(String[] args) {
        try {
            dangerousMethod();
        } catch (RuntimeException e) {
            System.out.println("Caller sees: " + e.getMessage());
            System.out.println("Cause: " + e.getCause());
            // "DATABASE CORRUPTION" is completely GONE.
            // Not as cause, not as suppressed, just lost.
        }
    }
}
TEXT
Caller sees: Failed to close connection
Cause: null

What happens when finally returns a value?

A return statement in finally overrides the return value from try or catch. The caller receives the finally value, not the try/catch value.

JAVA
package com.bank.demo;

import java.math.BigDecimal;

public class FinallyReturnOverrideDemo {

    // WRONG: finally return overrides try return
    public static int wrongReturn() {
        try {
            return 1;  // try says 1
        } finally {
            return 2;  // finally says 2, caller gets 2
        }
    }

    // WRONG: finally return swallows exception entirely
    public static String wrongSwallow() {
        try {
            throw new RuntimeException("CRITICAL: Settlement data corrupted");
        } finally {
            return "Settlement completed successfully";
            // Exception is GONE. Method appears to succeed.
        }
    }

    public static void main(String[] args) {
        System.out.println("Return override: " + wrongReturn());
        System.out.println("Exception swallowed: " + wrongSwallow());
    }
}
TEXT
Return override: 2
Exception swallowed: Settlement completed successfully

What is the correct pattern for cleanup in finally?

Catch cleanup exceptions inside finally and log them, but never re-throw them. This preserves the original exception from the try block.

JAVA
package com.bank.demo;

public class CorrectFinallyPattern {

    // CORRECT: catch cleanup exceptions, log them, do not re-throw
    public static void safeCleanup() {
        String resource = null;
        try {
            resource = "DatabaseConnection-SAV2001";
            System.out.println("Opened: " + resource);
            throw new RuntimeException("Trade settlement failed");
        } catch (RuntimeException e) {
            System.out.println("Caught: " + e.getMessage());
            throw e;  // re-throw to caller
        } finally {
            if (resource != null) {
                try {
                    System.out.println("Closing: " + resource);
                    // Simulate close failure:
                    // throw new RuntimeException("Close failed");
                } catch (RuntimeException closeEx) {
                    System.out.println("WARN: close failed: " + closeEx.getMessage());
                    // Log but do NOT re-throw. Preserves try's exception.
                }
            }
        }
    }

    public static void main(String[] args) {
        try {
            safeCleanup();
        } catch (RuntimeException e) {
            System.out.println("Caller correctly sees: " + e.getMessage());
        }
    }
}
TEXT
Opened: DatabaseConnection-SAV2001
Caught: Trade settlement failed
Closing: DatabaseConnection-SAV2001
Caller correctly sees: Trade settlement failed

How does try-with-resources solve this problem?

try-with-resources handles the dual-exception scenario correctly. The try body's exception is the primary (propagated to caller), and close()'s exception is suppressed (attached via addSuppressed()).

JAVA
package com.bank.demo;

public class TryWithResourcesSolution {

    static class BankResource implements AutoCloseable {
        private final String name;

        BankResource(String name) {
            this.name = name;
            System.out.println("Opened: " + name);
        }

        public void process() {
            throw new RuntimeException("TRADE_FAILED: invalid exchange rate");
        }

        @Override
        public void close() {
            System.out.println("Closing: " + name);
            throw new RuntimeException("CLOSE_FAILED: connection reset");
        }
    }

    public static void main(String[] args) {
        try (BankResource resource = new BankResource("SettlementConn")) {
            resource.process();
        } catch (RuntimeException e) {
            System.out.println("Primary: " + e.getMessage());
            for (Throwable suppressed : e.getSuppressed()) {
                System.out.println("Suppressed: " + suppressed.getMessage());
            }
        }
        // BOTH exceptions preserved. Nothing lost.
    }
}
TEXT
Opened: SettlementConn
Closing: SettlementConn
Primary: TRADE_FAILED: invalid exchange rate
Suppressed: CLOSE_FAILED: connection reset

What are the common mistakes?

JAVA
package com.bank.demo;

public class FinallyMistakesDemo {

    // WRONG: return in finally overrides try
    public static int mistake1() {
        try { return 100; }
        finally { return 200; }  // Caller gets 200, not 100
    }

    // WRONG: throw in finally swallows try's exception
    public static void mistake2() {
        try { throw new RuntimeException("Real error"); }
        finally { throw new RuntimeException("Cleanup error"); }
        // Real error is GONE
    }

    // WRONG: return in finally swallows try's exception
    public static String mistake3() {
        try { throw new RuntimeException("Critical failure"); }
        finally { return "OK"; }  // Exception vanishes
    }

    // CORRECT: no return/throw in finally
    public static int correct() {
        try {
            return 100;
        } finally {
            System.out.println("Cleanup only, no return");
        }
    }

    public static void main(String[] args) {
        System.out.println("Mistake 1 (return override): " + mistake1());
        System.out.println("Mistake 3 (swallowed exception): " + mistake3());
        System.out.println("Correct: " + correct());
    }
}
TEXT
Mistake 1 (return override): 200
Mistake 3 (swallowed exception): OK
Cleanup only, no return
Correct: 100

What should a senior engineer know?

  • SonarQube flags this. Rule squid:S1143 ("return or throw should not be in finally") is rated as a critical issue. Code reviews should block this pattern.
  • try-with-resources is structurally superior. It makes the dual-exception problem impossible by design. The JVM automatically calls addSuppressed() for close exceptions.
  • Suppressed exceptions are visible in stack traces. When using try-with-resources, the "Suppressed:" section appears in the stack trace output, preserving all information.
  • Multiple close failures. If multiple resources in try-with-resources each fail on close, each failure is added as a separate suppressed exception.

What are common interview follow-up questions?

  • What is the Throwable.addSuppressed() method? (Attaches a secondary exception to the primary one.)
  • How do you access suppressed exceptions? (Call e.getSuppressed() which returns Throwable[].)
  • What is the difference between suppressed and caused-by? (Suppressed is for dual-throw scenarios. Caused-by is for exception chaining/wrapping.)
  • Why does the compiler warn about return in finally? (Because it silently overrides the try/catch result.)

Visual Overview

DIAGRAM
  Scenario: try throws A, finally throws B
  ─────────────────────────────────────────

  OLD (finally-throw):
  ┌───────────┐    ┌───────────┐
  │   try      │    │  finally   │
  │ throws A   │───▶│ throws B   │───▶ Caller sees B
  └───────────┘    └───────────┘
       A is GONE. Not suppressed. LOST.

  NEW (try-with-resources):
  ┌───────────┐    ┌───────────┐
  │   try      │    │  close()   │
  │ throws A   │───▶│ throws B   │───▶ Caller sees A
  └───────────┘    └───────────┘     + B as suppressed
       BOTH preserved. A = primary, B = suppressed.

Key Takeaway

Throwing or returning from a finally block is one of the most dangerous patterns in Java. If the try block throws ExceptionA and the finally block throws ExceptionB, ExceptionA is silently and completely discarded. It is not attached as a cause, not added as suppressed, just gone. The caller only sees ExceptionB, which is typically a meaningless cleanup error, while the real problem is invisible.

Returning from finally is equally dangerous. If try returns 100 and finally returns 200, the caller gets 200. If try throws a critical exception and finally returns a value, the exception vanishes entirely and the method appears to succeed. In a banking scenario at National Bank of India, this means a settlement failure is silently swallowed and Priya is told her transfer completed when it actually failed.

try-with-resources solves this by design. When both the try body and close() throw, the try body's exception is primary (propagated to the caller) and close()'s exception is suppressed (attached via addSuppressed, visible in stack traces). Both exceptions are preserved, nothing is lost.

Remember one sentence: never return or throw from finally because it silently destroys the real exception, and use try-with-resources which preserves both exceptions automatically.

---

Share
Use ← → arrow keys to navigate