Definition
- If
trythrows ExceptionA andfinallythrows ExceptionB, ExceptionA is silently discarded. Not suppressed, not attached as a cause, completely gone. The caller only sees ExceptionB. - If
tryreturns value X andfinallyreturns value Y, the caller receives Y. Value X is silently overridden. - If
trythrows an exception andfinallyreturns 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.
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.
}
}
}
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.
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());
}
}
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.
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());
}
}
}
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()).
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.
}
}
Opened: SettlementConn
Closing: SettlementConn
Primary: TRADE_FAILED: invalid exchange rate
Suppressed: CLOSE_FAILED: connection reset
What are the common mistakes?
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());
}
}
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 returnsThrowable[].) - 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
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.
---