Definition
- When both the try body and
close()throw in try-with-resources, the try body's exception is the primary (propagated to the caller) and close()'s exception is suppressed (attached viaThrowable.addSuppressed()). - Call
e.getSuppressed()to retrieve aThrowable[]of all suppressed exceptions. - In the pre-Java-7 finally pattern, if finally threw, the try body's exception was silently replaced. Not suppressed, not attached, completely gone.
- Multiple resources: if several resources each fail on close, each failure is added as a separate suppressed exception to the primary.
- You can manually call
addSuppressed()in your own batch processing code to accumulate all errors into one exception.
In the old system (finally), when the vault caught fire (try error) and the fire extinguisher malfunctioned (finally error), the incident report only said "extinguisher malfunction." The fire was never reported. In the new system (try-with-resources), the report says: "PRIMARY: vault fire. SUPPRESSED: extinguisher malfunction." Both are recorded and investigators see the complete picture.
Understanding the Concepts
What are suppressed exceptions?
Suppressed exceptions are secondary exceptions that occur during resource cleanup in try-with-resources. They are attached to the primary exception (from the try body) rather than replacing it.
package com.bank.demo;
public class SuppressedExceptionDemo {
static class TradeConnection implements AutoCloseable {
@Override
public void close() {
throw new RuntimeException("CLOSE_FAILED: connection reset");
}
}
public static void main(String[] args) {
try (TradeConnection conn = new TradeConnection()) {
throw new IllegalStateException("TRADE_FAILED: invalid exchange rate");
} catch (Exception e) {
System.out.println("Primary: " + e.getMessage());
System.out.println("Suppressed count: " + e.getSuppressed().length);
for (Throwable suppressed : e.getSuppressed()) {
System.out.println("Suppressed: " + suppressed.getMessage());
}
}
}
}
Primary: TRADE_FAILED: invalid exchange rate
Suppressed count: 1
Suppressed: CLOSE_FAILED: connection reset
How do multiple suppressed exceptions work?
When multiple resources each fail on close, each failure is added as a separate suppressed exception.
package com.bank.demo;
public class MultipleSuppressedDemo {
static class Resource implements AutoCloseable {
private final String name;
Resource(String name) { this.name = name; }
@Override public void close() {
throw new RuntimeException("CLOSE_" + name);
}
}
public static void main(String[] args) {
try (Resource r1 = new Resource("CONN");
Resource r2 = new Resource("STMT");
Resource r3 = new Resource("RS")) {
throw new RuntimeException("MAIN_ERROR: settlement failed");
} catch (RuntimeException e) {
System.out.println("Primary: " + e.getMessage());
for (Throwable s : e.getSuppressed()) {
System.out.println("Suppressed: " + s.getMessage());
}
// Closed in reverse: RS, STMT, CONN
// Each close failure is a separate suppressed exception
}
}
}
Primary: MAIN_ERROR: settlement failed
Suppressed: CLOSE_RS
Suppressed: CLOSE_STMT
Suppressed: CLOSE_CONN
How is this better than the pre-Java-7 finally pattern?
In the old pattern, a throw in finally completely replaces the try exception. The real error is gone.
package com.bank.demo;
public class OldVsNewComparison {
// OLD: finally throw REPLACES try exception
public static void oldPattern() {
try {
throw new RuntimeException("TRADE_FAILED");
} finally {
throw new RuntimeException("CLOSE_FAILED");
}
// Caller sees ONLY: CLOSE_FAILED
// TRADE_FAILED is GONE. Not suppressed. LOST.
}
// NEW: try-with-resources preserves BOTH
public static void newPattern() {
try (AutoCloseable resource = () -> {
throw new RuntimeException("CLOSE_FAILED");
}) {
throw new RuntimeException("TRADE_FAILED");
} catch (Exception e) {
System.out.println("Primary: " + e.getMessage());
System.out.println("Suppressed: " + e.getSuppressed()[0].getMessage());
}
}
public static void main(String[] args) {
System.out.println("=== Old pattern ===");
try {
oldPattern();
} catch (RuntimeException e) {
System.out.println("Caller sees: " + e.getMessage());
System.out.println("TRADE_FAILED is LOST\n");
}
System.out.println("=== New pattern ===");
newPattern();
}
}
=== Old pattern ===
Caller sees: CLOSE_FAILED
TRADE_FAILED is LOST
=== New pattern ===
Primary: TRADE_FAILED
Suppressed: CLOSE_FAILED
How do you manually add suppressed exceptions?
You can call addSuppressed() in your own code. This is useful for batch processing where you want to collect all errors into one exception.
package com.bank.service;
import java.math.BigDecimal;
import java.util.List;
public class BatchSettlementService {
public static void settleBatch(List<String> tradeIds) {
RuntimeException primary = null;
for (String tradeId : tradeIds) {
try {
settle(tradeId);
System.out.println("Settled: " + tradeId);
} catch (RuntimeException e) {
if (primary == null) {
primary = e; // first error becomes primary
} else {
primary.addSuppressed(e); // subsequent errors are suppressed
}
}
}
if (primary != null) {
throw primary; // one exception with ALL failures attached
}
}
private static void settle(String tradeId) {
if (tradeId.contains("BAD")) {
throw new RuntimeException("Settlement failed for " + tradeId);
}
}
public static void main(String[] args) {
try {
settleBatch(List.of("TRD-001", "TRD-BAD-002", "TRD-003", "TRD-BAD-004"));
} catch (RuntimeException e) {
System.out.println("\nPrimary: " + e.getMessage());
for (Throwable s : e.getSuppressed()) {
System.out.println("Suppressed: " + s.getMessage());
}
}
}
}
Settled: TRD-001
Settled: TRD-003
Primary: Settlement failed for TRD-BAD-002
Suppressed: Settlement failed for TRD-BAD-004
What does the stack trace look like with suppressed exceptions?
Logging frameworks like SLF4J and Logback automatically include suppressed exceptions in the output.
package com.bank.demo;
public class SuppressedStackTraceDemo {
public static void main(String[] args) {
try (AutoCloseable r = () -> { throw new Exception("CLOSE_ERROR"); }) {
throw new RuntimeException("MAIN_ERROR");
} catch (Exception e) {
e.printStackTrace();
// Output includes:
// java.lang.RuntimeException: MAIN_ERROR
// at ...
// Suppressed: java.lang.Exception: CLOSE_ERROR
// at ...
}
}
}
java.lang.RuntimeException: MAIN_ERROR
at com.bank.demo.SuppressedStackTraceDemo.main(SuppressedStackTraceDemo.java:6)
Suppressed: java.lang.Exception: CLOSE_ERROR
at com.bank.demo.SuppressedStackTraceDemo.lambda$main$0(SuppressedStackTraceDemo.java:5)
What if only close() throws but try succeeds?
If the try body completes normally and close() throws, the close exception becomes the primary (not suppressed) because there is no try-body exception to attach it to.
package com.bank.demo;
public class CloseOnlyThrowsDemo {
public static void main(String[] args) {
try (AutoCloseable r = () -> { throw new Exception("Close failed"); }) {
System.out.println("Try body succeeded");
} catch (Exception e) {
System.out.println("Exception from close: " + e.getMessage());
System.out.println("Suppressed count: " + e.getSuppressed().length);
}
}
}
Try body succeeded
Exception from close: Close failed
Suppressed count: 0
What should a senior engineer know?
- Throwable.addSuppressed() was added in Java 7 specifically for try-with-resources, but it is a public method you can use anywhere.
- Performance. addSuppressed() is not free. Each call allocates or resizes an internal array. For normal usage (a few suppressed per exception), this is negligible.
- Serialization. Suppressed exceptions are serialized along with the primary exception. They survive RPC boundaries and log aggregation.
- Log aggregation. ELK Stack, Splunk, and CloudWatch all display suppressed exceptions from the stack trace. No special configuration needed.
What are common interview follow-up questions?
- What is the difference between suppressed and caused-by? (Suppressed is for concurrent exceptions from close(). Caused-by is for exception chaining/wrapping.)
- Can you have both suppressed and caused-by? (Yes. A primary exception can have a cause AND suppressed exceptions.)
- What happens if addSuppressed is called with the exception itself? (
e.addSuppressed(e)throws IllegalArgumentException.) - Is suppressed transitive? (No. Each exception has its own getSuppressed() array.)
Visual Overview
OLD (finally-throw): NEW (try-with-resources):
┌─────────────────────┐ ┌─────────────────────────┐
│ try throws A │ │ try throws A │
│ finally throws B │ │ close() throws B │
│ │ │ │
│ Result: B only │ │ Result: │
│ A is LOST │ │ Primary = A │
└─────────────────────┘ │ Suppressed = B │
│ Both visible │
└─────────────────────────┘
getSuppressed():
┌─────────────────────────────────┐
│ Primary: TRADE_FAILED │
│ getSuppressed()[0]: CLOSE_R3 │
│ getSuppressed()[1]: CLOSE_R2 │
│ getSuppressed()[2]: CLOSE_R1 │
└─────────────────────────────────┘
Key Takeaway
Suppressed exceptions are Java 7's solution to the dual-exception problem. In try-with-resources, if both the try body and close() throw, the try body's exception is primary (propagated to the caller) and close()'s exception is suppressed (attached via addSuppressed(), retrievable via getSuppressed()). Both are visible in stack traces. Nothing is lost.
In the pre-Java-7 finally pattern, a throw in finally silently replaced the try body's exception. The real error vanished completely. At National Bank of India, the settlement service threw "SETTLEMENT_FAILED" but the finally block's lock release threw "LOCK_RELEASE_FAILED." Ops investigated locks for hours while the real problem (bad exchange rate) went undetected. With try-with-resources, both exceptions are preserved.
You can manually call addSuppressed() in your own code for batch processing. The first failure becomes the primary, and all subsequent failures are attached as suppressed. The caller catches one exception containing all failures, which is cleaner than collecting errors in a list.
Remember one sentence: suppressed exceptions preserve both the try-body failure and the close failure, while the old finally pattern silently destroyed one of them.
---