Where does the close() exception go? Reading suppressed exceptions in Java try-with-resources
When both the try block and close() throw, which exception propagates and which ends up in getSuppressed()? Close order and multiple failures, per JLS 14.20.3.
If the body of a try-with-resources statement throws, and the resource’s close() throws too, which exception does the caller get? The Java Language Specification (JLS) answers this precisely. The exception from the try block propagates, and the one from close() is attached to it as a suppressed exception. It isn’t lost, but code that logs only the message won’t show it.
This post uses the Java SE 25 documentation as its baseline: JLS §14.20.3 and the Throwable and AutoCloseable API pages, checked on 2026-10-12. Java SE 25 is the reference point here, not a claim that it is the newest release. According to the API docs, addSuppressed() and getSuppressed() have existed since Java 1.7.
The example program
Every scenario in this post comes from the single file below. Res prints its name when it is created and when it is closed, and throws an IllegalStateException from close() when failOnClose is true. run() prints the message of whatever the scenario throws, followed by the contents of getSuppressed().
public class TwrDemo {
static final class Res implements AutoCloseable {
private final String name;
private final boolean failOnClose;
Res(String name, boolean failOnClose) {
this.name = name;
this.failOnClose = failOnClose;
System.out.println("open " + name);
}
@Override
public void close() {
System.out.println("close " + name);
if (failOnClose) {
throw new IllegalStateException("close " + name + " failed");
}
}
}
static void bodyAndCloseFail() {
try (Res a = new Res("A", true)) {
throw new RuntimeException("body failed");
}
}
static void onlyCloseFails() {
try (Res a = new Res("A", true)) {
System.out.println("body ok");
}
}
static void closeOrder() {
try (Res a = new Res("A", false); Res b = new Res("B", false)) {
System.out.println("body ok");
}
}
static void twoClosesFail() {
try (Res a = new Res("A", true); Res b = new Res("B", true)) {
System.out.println("body ok");
}
}
static void bodyAndTwoClosesFail() {
try (Res a = new Res("A", true); Res b = new Res("B", true)) {
throw new RuntimeException("body failed");
}
}
static void naiveFinally() {
Res a = new Res("A", true);
try {
throw new RuntimeException("body failed");
} finally {
a.close();
}
}
static void run(String label, Runnable scenario) {
System.out.println("== " + label);
try {
scenario.run();
System.out.println("no exception");
} catch (RuntimeException e) {
System.out.println("thrown: " + e.getMessage());
for (Throwable s : e.getSuppressed()) {
System.out.println(" suppressed: " + s.getMessage());
}
}
}
public static void main(String[] args) {
run("1 body + close", TwrDemo::bodyAndCloseFail);
run("2 close only", TwrDemo::onlyCloseFails);
run("3 close order", TwrDemo::closeOrder);
run("4 two closes", TwrDemo::twoClosesFail);
run("5 body + two closes", TwrDemo::bodyAndTwoClosesFail);
run("6 naive finally", TwrDemo::naiveFinally);
}
}
Compile with javac TwrDemo.java and run with java TwrDemo. The output shown below, split by scenario, is what the rules in JLS §14.20.3 produce.
When both the body and close() fail
== 1 body + close
open A
close A
thrown: body failed
suppressed: close A failed
The caller receives body failed, the exception thrown in the try block, and close A failed sits in its getSuppressed() array. JLS §14.20.3.1 says that if the try block throws a value V and the automatic close throws V2, the statement throws V, with V2 added to V’s suppressed exception list.
The translation given in the same section makes the mechanics clear. The compiler-generated finally checks whether a primary exception (#primaryExc) was already caught. If so, it wraps close() in its own try and attaches whatever comes out with #primaryExc.addSuppressed(...).
When only close() fails
== 2 close only
open A
body ok
close A
thrown: close A failed
The try block finished normally, so there is no primary exception to attach anything to. The exception from close() propagates as is, the suppressed array is empty, and no suppressed: line is printed. In the translation, this is the branch where #primaryExc is null and close() is called directly without a wrapper.
Resources close in reverse declaration order
== 3 close order
open A
open B
body ok
close B
close A
no exception
Resources are initialized left to right and closed in the reverse order (§14.20.3). A statement with several resources is treated as nested single-resource try-with-resources statements, so B, opened last, is the inner one and closes first.
When several close() calls fail
An exception from closing one resource does not stop the others from being closed. If an exception was already thrown earlier by an initializer, the try block, or the closing of another resource, any later close exception becomes suppressed (§14.20.3).
Start with a body that succeeds while both close() calls fail.
== 4 two closes
open A
open B
body ok
close B
close A
thrown: close B failed
suppressed: close A failed
The exception that propagates is close B failed. The resource that closes first, which is the rightmost one in the declaration, supplies the primary exception, and the leftmost resource’s exception is attached to it. The JLS phrases this as V1 (from the rightmost resource failing to close) being thrown, with V2…Vn added to V1’s suppressed list. If you remember it as “the first declared resource wins,” you will read the trace backwards.
When the body fails as well, the body’s exception is primary again.
== 5 body + two closes
open A
open B
close B
close A
thrown: body failed
suppressed: close B failed
suppressed: close A failed
Following the nested translation, the inner statement closes B and attaches close B failed to the body exception, then the outer statement closes A and attaches close A failed to the same exception. addSuppressed() appends to the list, so the array holds B then A, in the order they were closed. The sample output in the Throwable.printStackTrace() documentation has the same shape, with Resource ID = 2 listed before Resource ID = 1.
Failures during initialization follow the same rule. If a resource’s initializer throws, only the resources initialized so far are closed, and any exception from closing them is attached to the initializer’s exception as suppressed.
Inspecting getSuppressed()
Throwable.getSuppressed() returns an array of the exceptions that were suppressed in order to deliver this one. It returns an empty array when nothing was suppressed, and writing to the returned array does not affect later calls.
There is one more case that yields an empty array. If an exception class disabled suppression through the four-argument Throwable constructor, then addSuppressed() does nothing beyond validating its argument. If such an exception is the primary one, the close() exception is not recorded anywhere.
printStackTrace() prints suppressed exceptions beneath the primary exception’s stack trace, indented further. The exact format depends on the implementation, but in the documentation’s examples they appear on lines starting with Suppressed:. Code that records only getMessage(), like run() above, will not show a close failure unless it walks getSuppressed() itself. The addSuppressed() documentation also notes that a cause (getCause()) and suppressed exceptions are separate relationships, and a single exception can have both.
Closing it yourself in finally
== 6 naive finally
open A
close A
thrown: close A failed
naiveFinally() skips try-with-resources and calls close() from a finally block. The body’s body failed appears nowhere in the result. Under JLS §14.20.2, if the try block throws V and the finally block completes abruptly for reason S, the try statement completes abruptly for reason S, and V is discarded.
To be precise, the body exception is lost only when the close() inside finally actually throws. If close() completes normally, the body exception propagates as usual. Not every handwritten finally behaves this way either. If you keep the primary exception in a variable, wrap close() in its own try, and attach its exception with addSuppressed(), as the JLS translation does, you get the same result. try-with-resources is the compiler writing that code for you.
Adding catch or finally
When you add catch or finally to try-with-resources, JLS §14.20.3.2 treats it as a basic try-with-resources statement wrapped in an outer try statement. By the time a catch or finally block runs, every resource has been closed or a close has been attempted, and that catch can also catch exceptions from initializing or closing a resource.
Don’t throw InterruptedException from close()
The API note on AutoCloseable.close() strongly advises implementers not to throw InterruptedException from close(). The stated reason is that this exception interacts with a thread’s interrupted status, and runtime misbehavior is likely if it gets suppressed. The note adds, more generally, that if suppressing an exception would cause problems, close() should not throw it.
The same note also advises that when close() can fail, it should release the underlying resources and mark the resource as closed before throwing, since close() is unlikely to be called more than once.
Sources
All checked on 2026-10-12.
- Java Language Specification, Java SE 25, §14.20.2 Execution of try-finally and try-catch-finally
- Java Language Specification, Java SE 25, §14.20.3 try-with-resources
- Java SE 25 API: Throwable.getSuppressed()
- Java SE 25 API: Throwable.addSuppressed(Throwable)
- Java SE 25 API: Throwable.printStackTrace()
- Java SE 25 API: AutoCloseable.close()