Suppressed exceptions in Java
Exceptions from close() attached via getSuppressed().
Two failures at once
In try-with-resources, if the body throws and then close() throws too, the body's exception stays primary — it's the one that propagates. The close() failure is attached to it as a suppressed exception via addSuppressed(), so nothing is lost.
Count the suppressed
Both the body and close() throw. What prints?
AutoCloseable res = () -> {
throw new IllegalStateException("close");
};
try (res) {
throw new RuntimeException("body");
} catch (Exception e) {
System.out.println(e.getMessage());
System.out.println(e.getSuppressed().length);
}body 1close 1body 0
Show the answer
The body's exception is primary, so its message is body. The close() failure sits in its suppressed array, which has length 1.
Reading them
**e.getSuppressed()** returns a Throwable[] with everything attached. Printing one shows its class and message, like java.io.IOException: C. Stack traces list them under **Suppressed:. And addSuppressed(Throwable)** is public (Java 7+), so your own cleanup code can attach them too.
catch (Exception e) {
for (Throwable s : e.getSuppressed())
System.out.println(s);
}Plain finally loses the original
With an ordinary try/finally, an exception thrown in finally simply replaces the one in flight. Here the catch sees only cleanup — original is gone forever. That's exactly the problem suppressed exceptions solve.
try {
try {
throw new RuntimeException("original");
} finally {
throw new IllegalStateException("cleanup");
}
} catch (RuntimeException e) {
System.out.println(e.getMessage()); // cleanup
}Why the body wins
Why does Java keep the body's exception as primary rather than the one from close()?
Think about it, then reveal the answer
The body's exception is usually the real reason things went wrong; a failing close() is often just a consequence. Keeping it primary — with the close() failure attached — tells the whole story in the right order.
Debugging with the full story
Before Java 7, a database error was often hidden behind a "connection reset" thrown while closing — engineers chased the wrong bug for hours. Today, scroll down to Suppressed: in the log and both failures are there.
Key takeaways
- Primary exception = the one thrown by the try body
- close() failures are attached with addSuppressed()
- Read them with e.getSuppressed() (an array)
- Stack traces list them under 'Suppressed:'
Throwable has a protected constructor with an enableSuppression flag. Pass false and addSuppressed quietly does nothing.
Practice questions
What does this print?
AutoCloseable res = () -> { throw new IOException("C"); };
try (res) {
throw new RuntimeException("B");
} catch (Exception e) {
System.out.println(e.getMessage());
System.out.println(e.getSuppressed()[0]);
}- B java.io.IOException: C
- C java.lang.RuntimeException: B
- B null
- Throws IOException
Check your answer
B java.io.IOException: C. The body's RuntimeException is primary. The IOException from close() is stored in its suppressed array.
How do you read the exceptions that were suppressed during cleanup?
- Call getSuppressed() on the primary exception — it returns an array
- Call getCause() repeatedly
- They go only to System.err and can't be read
- Add a second catch block for them
Check your answer
Call getSuppressed() on the primary exception — it returns an array. getSuppressed() returns a Throwable[] with everything attached via addSuppressed(), which try-with-resources does automatically.