skip to content

What are suppressed exceptions in try-with-resources, and what happens when both the try body and close() throw?

level: seniorimportance: must knowfreq 58%

answer

  1. old finally masked the body exception
  2. body exception = primary, propagates
  3. close() exception attached via addSuppressed()
  4. read with getSuppressed(), shown as 'Suppressed:' in trace
  5. body OK + close throws -> close exception is primary

basics

~20 s

If the code in the try block throws an error and then close() also throws, Java keeps the first error (from the body) as the one you see, and attaches the close() error to it as a suppressed exception, which you can read later with getSuppressed().

solid answer

~50 s

In the old try/finally idiom, if the body threw and then close() in finally also threw, the close() exception replaced the original, masking the real cause. try-with-resources fixes this: when both the body and a resource's close() throw, the body's exception is the primary exception that propagates, and each close() exception is added to it as a suppressed exception via Throwable.addSuppressed(). You retrieve them with Throwable.getSuppressed(), and they print under a 'Suppressed:' line in the stack trace. The reverse case also holds: if the body completes normally but close() throws, that close() exception propagates as the primary one (nothing to suppress). With multiple resources, each failing close() contributes one suppressed exception. The design choice is that the body's failure is usually the more meaningful diagnostic, so it is preserved, while no cleanup failure is silently lost.

code

java · 13 lines
java
class Bad implements AutoCloseable {
    @Override public void close() { throw new RuntimeException("CLOSE failed"); }
}

try {
    try (Bad b = new Bad()) {
        throw new IllegalStateException("BODY failed");
    }
} catch (Exception e) {
    System.out.println("primary: " + e.getMessage());          // BODY failed
    for (Throwable s : e.getSuppressed())
        System.out.println("suppressed: " + s.getMessage());   // CLOSE failed
}

go deeper

for a junior

Knows that if close() also fails, Java does not lose the original error.

for a middle

Explains that the body exception propagates and the close() exception is attached as suppressed, retrievable via getSuppressed().

for a senior

Describes both directions (body+close fail vs only close fails), the addSuppressed/getSuppressed API, multiple-resource behavior, and contrasts with the old masking idiom.

for a principal

Distinguishes suppression from causal chaining, reasons about diagnostics/observability of cleanup failures, and sets team conventions for handling suppressed exceptions in logging.

## The masking problem in the old idiom Consider the pre-Java-7 pattern: ```java Reader r = open(); try { throw new IllegalStateException("BODY failed"); // (1) the real problem } finally { r.close(); // (2) suppose this also throws IOException("CLOSE failed") } ``` In Java, when a `finally` block throws, its exception **replaces** any exception from the `try` body. So the caller sees only `IOException("CLOSE failed")` — the genuine cause `(1)` is **lost**. This "exception masking" hid the actual bug and made debugging painful. ## What try-with-resources does instead try-with-resources changes the rule. When the body throws an exception **E1** and then a resource's `close()` throws **E2**: - **E1 is the primary exception** that propagates to the caller. - **E2 is attached to E1 as a suppressed exception**, using the method `Throwable.addSuppressed(E2)` that the compiler inserts automatically. - Nothing is lost: the caller sees E1 and can reach E2. ```java try (Resource res = open()) { // close() will throw throw new IllegalStateException("BODY failed"); } catch (Exception e) { System.out.println(e); // IllegalStateException: BODY failed for (Throwable s : e.getSuppressed()) System.out.println("suppressed: " + s); // suppressed: IOException: CLOSE failed } ``` ## The Throwable API Two methods on `java.lang.Throwable` support this: - **`addSuppressed(Throwable t)`** — records `t` as suppressed by this exception. The compiler calls it; you rarely call it yourself. - **`getSuppressed()`** — returns a `Throwable[]` of all suppressed exceptions, in the order they were added. In a printed stack trace, suppressed exceptions appear under a line beginning with `Suppressed:` beneath the primary exception's trace. ## The other direction: body succeeds, close() fails If the body completes **normally** but `close()` throws, there is no primary exception from the body, so the **close() exception becomes the primary** one that propagates. There is nothing to suppress in this case. ## Multiple resources With several resources, each one's failing `close()` adds **one** suppressed exception to the primary. They are added in close order (reverse of declaration). So a body failure plus two failing closes yields one primary exception with two suppressed exceptions. ## Why this design The exception thrown by the **body** is almost always the more informative one — it is what your business logic was actually doing. A failure to close a resource is usually a secondary symptom. So try-with-resources preserves the body's exception as primary while still surfacing every cleanup failure for completeness. This is strictly better than the old idiom, which could throw away the real cause. ## Glossary - **Primary exception**: the exception that actually propagates out of the statement. - **Suppressed exception**: a secondary exception attached to the primary instead of replacing it. - **Masking**: when a later exception (e.g. from finally/close) hides an earlier one. - **Throwable**: the superclass of all exceptions and errors; it carries the suppressed list. Key sentence: *body exception wins and propagates; close() exceptions are attached via addSuppressed() and read with getSuppressed(), so nothing is silently masked.*

  • How do suppressed exceptions differ from the 'caused by' chain (getCause)?
    getCause models causality: exception A was thrown because of B (wrapping). getSuppressed models concurrency of unrelated failures: the primary exception happened, and separately a cleanup failure occurred that we did not want to lose. One is 'because of', the other is 'and also'.
  • Does plain try/finally produce suppressed exceptions automatically?
    No. Suppression is generated by the try-with-resources statement (and Throwable.addSuppressed). In a manual try/finally, a finally exception still masks the body exception unless you call addSuppressed yourself.

saying these in an interview costs you the question

  • Saying the close() exception masks/replaces the body exception (it is the reverse)
  • Believing the close() exception is silently discarded
  • Confusing suppressed exceptions with chained 'caused by' (getCause) exceptions
  • Thinking suppression also happens in plain try/finally automatically (it does not)

context