Back to try-with-resources & Suppressed Exceptions
Q2Question 2 of 2

What are suppressed exceptions?

Key Concepts
  • In try-with-resources, the try body exception is primary (propagated) and close() exception is suppressed (attached via addSuppressed)
  • getSuppressed() returns a Throwable array of all suppressed exceptions, preserving complete error information
  • In the old finally pattern, a throw in finally silently replaces the try exception, which is completely lost
  • Multiple resources each failing on close produce multiple suppressed exceptions attached to the primary
  • You can manually call addSuppressed() in your own code for batch processing scenarios
  • Stack trace output includes a Suppressed section showing each attached exception with its full trace

Definition

Suppressed exceptions preserve close() failures by attaching them to the primary try-body exception via addSuppressed(), unlike the old finally pattern where the try exception was silently lost.
  • 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 via Throwable.addSuppressed()).
  • Call e.getSuppressed() to retrieve a Throwable[] 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.

JAVA
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());
            }
        }
    }
}
TEXT
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.

JAVA
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
        }
    }
}
TEXT
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.

JAVA
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();
    }
}
TEXT
=== 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.

JAVA
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());
            }
        }
    }
}
TEXT
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.

JAVA
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 ...
        }
    }
}
TEXT
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.

JAVA
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);
        }
    }
}
TEXT
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

DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate