Suppressed Exceptions in Try-With-Resources
Fast recall
Quick revision
Core answer
close() also throws, the body failure stays primary and each close failure is recorded in getSuppressed().Interview-ready guide
Interview answer
A suppressed exception is a secondary Throwable stored on a primary Throwable so that one failure does not erase another. Try-with-resources uses this mechanism when the try body throws and one or more resource close() calls also throw during cleanup.
The body exception remains primary because it is the reason normal work failed and cleanup began. Each close failure is attached to that Throwable and can be inspected with getSuppressed(). This is better than a naive finally block, where a close exception can replace the original exception and hide the actual processing problem.
For example, suppose parsing throws IllegalStateException("invalid record"), and the reader's close method throws IOException because the underlying device also failed. The caller catches the IllegalStateException. Its suppressed array contains the IOException, so diagnostics can report both the invalid record and the failed cleanup.
If the body completes normally and one close throws, there is no earlier primary failure, so the close exception propagates. With several resources closing in reverse order, the first close failure encountered becomes primary in that case, and later close failures are attached to it. Java continues the required cleanup rather than abandoning remaining resources after one failed close.
Suppression preserves information but does not handle it automatically. Logging only exception.getMessage() can omit suppressed failures, while a normal full stack trace usually includes them. Code should not replace the primary exception casually, and operational diagnostics should retain the full Throwable chain. The rule is simple: primary explains the main exit; suppressed entries explain additional failures during cleanup.
Suppressed Exceptions in Try-With-Resources
Direct Answer
A suppressed exception is a secondary failure attached to the primary Throwable. In try-with-resources, when the body is already throwing and close() also throws, the body failure stays primary and each close failure is recorded in getSuppressed().
Deep Dive & Architecture
A scope can fail twice: the work can fail, and releasing the resource can fail. Only one Throwable can be thrown directly from a statement, so Java needs a rule for retaining both. Try-with-resources treats the failure already in flight as primary and adds later cleanup failures to it. The order reflects causality rather than claiming that cleanup is unimportant.
For multiple resources, cleanup still proceeds in reverse declaration order. A failure from the last resource does not prevent Java from trying to close earlier resources. Their failures join the suppressed list in the order encountered. This maximizes cleanup attempts and gives operators a fuller picture of a cascading outage.
Suppression is a general Throwable feature, but try-with-resources is its most common source. A class can call addSuppressed itself, subject to Throwable's restrictions, though ordinary application code rarely needs to. The important consumer API is getSuppressed, which returns the secondary Throwables.
Recovery decisions should stay tied to the primary operation. A failed write plus a failed close can mean output completeness is unknown; reporting both is useful, but neither proves whether bytes reached stable storage. Domain code still needs a policy for partial targets, retries, quarantine, or reconciliation.
Code Example
class SuppressedFailure {
static final class BrokenClose implements AutoCloseable {
public void close() throws Exception {
throw new Exception("close failed");
}
}
public static void main(String[] args) {
try {
try (var resource = new BrokenClose()) {
throw new IllegalStateException("body failed");
}
} catch (Exception failure) {
if (!"body failed".equals(failure.getMessage())) throw new AssertionError(failure);
if (failure.getSuppressed().length != 1
|| !"close failed".equals(failure.getSuppressed()[0].getMessage())) {
throw new AssertionError("missing suppressed close failure");
}
System.out.println(failure.getMessage() + " / "
+ failure.getSuppressed()[0].getMessage());
}
}
}
Check your understanding
Answer these without looking back at the article.
- What becomes primary when both the body and close throw?
- What happens when only close throws?
- How are several close failures retained?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.