skip to content

Explain how a finally block can hide the real exception, and how try-with-resources fixes this with suppressed exceptions.

level: seniorimportance: should knowfreq 55%

answer

  1. finally throws → original exception dropped (no chaining)
  2. return in finally also discards in-flight exception
  3. Java 7 suppressed exceptions = the fix
  4. try-with-resources does addSuppressed automatically
  5. Suppressed: vs Caused by: are different relationships

basics

~20 s

If the try body throws one exception and the finally block throws another (for example close() fails), the finally's exception replaces the original, so the real failure is lost. try-with-resources keeps the original and attaches the close failure as a suppressed exception instead.

solid answer

~50 s

When a try block throws exception A and its finally block then throws exception B, Java propagates B and silently abandons A. A finally that calls resource.close() can easily throw — a flush on close failing, for instance — so the original, more meaningful exception disappears and you are left debugging a misleading close error. try-with-resources solves this: the compiler closes resources after the body, and if both the body and a close() throw, the body's exception A stays primary and B is recorded on it via addSuppressed, retrievable with getSuppressed(). Printed traces show A plus a 'Suppressed:' section for B, so nothing is lost. If you must hand-write cleanup, replicate this: catch the body's exception, attempt cleanup in its own try/catch, and call primary.addSuppressed(cleanupEx) rather than letting the cleanup exception escape. The rule is that secondary failures during cleanup must never overwrite the primary failure.

code

java · 12 lines
java
// finally bug: parse error is lost, close error propagates
InputStream in = new FileInputStream(path);
try {
    parse(in);          // throws A (the real failure)
} finally {
    in.close();         // throws B -> B wins, A is gone
}

// try-with-resources: A propagates, B preserved as suppressed
try (InputStream in2 = new FileInputStream(path)) {
    parse(in2);         // A propagates; if close() throws B,
}                       // then A.getSuppressed() contains B

go deeper

for a junior

Recognizes that if both the try and the finally throw, you can lose the original error, and that try-with-resources is safer.

for a middle

Explains that finally's exception replaces the try's, names suppressed exceptions, and knows try-with-resources records the close failure rather than dropping the body's failure.

for a senior

Describes the addSuppressed/getSuppressed mechanism, the return-in-finally trap, and can hand-roll the safe primary/cleanup pattern when try-with-resources is not applicable.

for a principal

Reasons about diagnosability at scale (ensuring logs always surface the primary plus suppressed), library API design for cleanup that throws, and codifies try-with-resources as the standard to eliminate this bug class.

## The mechanics of how finally overrides A `finally` block runs no matter how the `try` exits. The subtle part is what happens to exceptions. Java's rule: **if the `try` block throws and the `finally` block also throws (or returns), the `finally`'s outcome wins** and the original exception is discarded. There is no chaining, no suppression — the original `Throwable` is simply dropped on the floor. Why this bites with resources: ```java InputStream in = new FileInputStream(path); try { parse(in); // throws ParseException A — the real problem } finally { in.close(); // throws IOException B (e.g. buffered flush fails) } ``` The caller receives **IOException B**. `ParseException A` — the thing you actually need to fix — is gone. You spend hours investigating a "close failed" error that is merely a side effect of the original failure. The same trap exists with `return` in finally: a `return` (or `break`/`continue`) inside `finally` discards any in-flight exception entirely. ## Suppressed exceptions: the fix (Java 7) Java 7 introduced **suppressed exceptions** specifically to solve this. A `Throwable` can carry a list of *suppressed* throwables — secondary failures that happened while the primary one was being handled. The API: - `void addSuppressed(Throwable t)` — attach a secondary failure. - `Throwable[] getSuppressed()` — retrieve them. **try-with-resources uses this automatically.** When the body throws A and a generated `close()` throws B, the compiler emits code equivalent to `A.addSuppressed(B)` and then throws **A**. So: - A is what propagates (the real failure). - B is preserved and reachable via `A.getSuppressed()`. - Printed traces include a `Suppressed:` block showing B's own trace. Nothing is lost, and the *primary* failure is the one you see first — the opposite of the finally bug. ## Reproducing the safe behavior by hand Sometimes you cannot use try-with-resources (e.g. the cleanup is not a simple `close()`). To stay safe, make cleanup failures suppressed, not overriding: ```java Throwable primary = null; try { doWork(); } catch (Throwable t) { primary = t; throw t; } finally { try { cleanup(); } catch (Exception cleanupEx) { if (primary != null) primary.addSuppressed(cleanupEx); else throw cleanupEx; // no primary, so this IS the failure } } ``` This is verbose and error-prone — which is exactly why try-with-resources is preferred whenever the resource is `AutoCloseable`. ## Cause vs suppressed (don't confuse them) - **Cause** (`getCause`): *why* this exception happened — the trigger, set when you wrap/translate. Shows as `Caused by:`. - **Suppressed** (`getSuppressed`): a *secondary* failure that occurred while handling the primary — typically a failed cleanup. Shows as `Suppressed:`. They are different relationships and both can appear in one printed trace. ## The principle A failure during cleanup is real information but must **never** replace the failure that started everything. The original exception is primary; cleanup failures are suppressed. try-with-resources gives you this for free; hand-written finally does not, so use try-with-resources whenever you can and replicate the addSuppressed pattern when you cannot.

  • How do you retrieve a suppressed exception programmatically?
    Call getSuppressed() on the primary Throwable; it returns a Throwable[] of secondary failures recorded via addSuppressed (e.g. failing close() calls from try-with-resources).
  • Why is a return statement inside finally dangerous?
    A return (or break/continue) in finally completes the block abruptly, which discards any exception that was propagating from the try body — silently swallowing the real failure, much like the finally-throws bug.

Imagine a paramedic (try body) collapses with a heart attack (exception A). A bystander trips on the way to help and twists an ankle (close failure B). The old finally behavior is like the ambulance only treating the twisted ankle and ignoring the heart attack. Suppressed exceptions keep treating the heart attack first and just note the ankle on the chart.

saying these in an interview costs you the question

  • Believing finally chains or suppresses the original exception automatically — it does not; the finally's exception simply replaces it.
  • Confusing suppressed exceptions with the cause chain.
  • Putting return/break in finally and assuming exceptions still propagate.
  • Thinking you must manually call addSuppressed inside try-with-resources — the compiler does it.

context