Question 24 · Section 7

What are suppressed exceptions?

Most commonly - in try-with-resources:

Language versions: English Russian Ukrainian

Junior Level

Definition

Suppressed exceptions - a Java 7+ mechanism that allows one exception to carry a list of other exceptions that occurred in parallel.

Where encountered

Most commonly - in try-with-resources:

try (Resource r = new Resource()) {
    r.doWork(); // IOException #1 - main
} // close() throws IOException #2 - suppressed

How to get them

try {
    // code
} catch (Exception e) {
    System.out.println("Main: " + e.getMessage());

    Throwable[] suppressed = e.getSuppressed();
    for (Throwable t : suppressed) {
        System.out.println("Suppressed: " + t.getMessage());
    }
}

Why it’s needed

Before Java 7, if exceptions occurred in both try and finally, the second erased the first (shadowing). Developers lost information about the root cause of the error.

Why specifically Java 7: in Java 7, try-with-resources appeared (JLS 14.20.3), which automatically closes resources. On automatic closing via close(), there’s a high probability that close() will throw an exception simultaneously with an exception from the main try. Without a suppressed mechanism, one exception would erase the other, making debugging impossible. Suppressed exceptions are an infrastructure addition to TWR, not a standalone feature.

When suppressed are NOT added

  1. Manual try-finally - in normal try { ... } finally { close(); } suppressed are NOT added automatically. Exception from finally shadows exception from try. Suppressed works only in try-with-resources or on manual call of addSuppressed().

  2. enableSuppression = false - if exception is created with new Exception(msg, cause, false, true), calls to addSuppressed() will be ignored.

  3. close() doesn’t throw - if resource closes correctly, suppressed list will be empty. This is a normal scenario.

  4. Exception in only one place - if only try failed (but not close()) or only close() failed (but not try), there won’t be suppressed - just one normal exception.

When NOT to rely on suppressed

  1. Manual try-finally - suppressed DON’T work automatically. If you write try { ... } finally { resource.close(); } and both can fail - information will be lost. Use try-with-resources.

  2. External libraries without TWR - if a third-party library doesn’t implement AutoCloseable or closes resources manually, suppressed won’t be added.

  3. Async context - in CompletableFuture or reactive streams, exceptions from different threads are not aggregated via suppressed automatically.

  4. Batch processing without pattern - if you process a batch of tasks and each can fail, suppressed won’t add themselves - you need to manually use addSuppressed().

When NOT to use suppressed exceptions

  1. Long-lived singleton objects - accumulation of suppressed exceptions leads to memory leak
  2. Serialization over network - entire suppressed list is transmitted over network, increasing payload
  3. Highload systems - each suppressed exception = object + stack trace in heap
  4. As replacement for normal error handling - suppressed don’t replace try-catch on each resource
  5. In business logic - suppressed are intended for infrastructure code (resources, batch processing)

Middle Level

Internal implementation

In Throwable there is a field private Throwable[] suppressedExceptions (array, not list - for performance).

  • Initialized lazily - only on first call to addSuppressed
  • Can be disabled on creation: new Exception(msg, cause, false, true) - enableSuppression = false

How compiler generates code for try-with-resources

When you write:

try (Resource r = new Resource()) {
    r.doWork();
}

The compiler expands this to (simplified):

Resource r = new Resource();
Throwable primaryException = null;
try {
    r.doWork();
} catch (Throwable e) {
    primaryException = e;
    throw e;
} finally {
    if (r != null) {
        if (primaryException != null) {
            try {
                r.close();
            } catch (Throwable closeException) {
                primaryException.addSuppressed(closeException);
            }
        } else {
            r.close(); // No main exception - close() throws as normal
        }
    }
}

Key point: compiler saves a reference to the main exception (primaryException) and on close() error adds it as suppressed to the main one, rather than replacing it.

Throwable constructor parameters

public Throwable(String message, Throwable cause,
                 boolean enableSuppression, boolean writableStackTrace)
  • enableSuppression - enables/disables suppressed (true by default)
  • writableStackTrace - if false, stack trace is not filled (speeds up exception creation)

Role in try-with-resources

Scenario:

  1. Reading file in try -> IOException (disk disconnected)
  2. TWR closes file -> second IOException (disk gone)
  3. In old Java the second would erase the first
  4. In modern: first comes to you, second - e.getSuppressed()

Manual usage

Although TWR does this automatically, you can use it manually:

Exception mainException = new BatchException("Batch failed");

for (Task task : tasks) {
    try {
        task.execute();
    } catch (Exception e) {
        mainException.addSuppressed(e);
    }
}

if (mainException.getSuppressed().length > 0) {
    throw mainException;
}

Limitations

  • Self-Suppression: e.addSuppressed(e) -> IllegalArgumentException
  • Null Suppression: e.addSuppressed(null) -> NullPointerException
  • Order: suppressed are stored in order of addition. In TWR - reverse order of resource declaration (LIFO)

Senior Level

Memory Leak Risk

Endless addition of suppressed exceptions to a long-lived object (singleton) - memory leak. Each exception carries its stack trace.

Serialization Cost

The entire list of suppressed exceptions is serialized with the main one. In distributed systems - huge volumes of data over the network.

Batch Processing pattern

public class BatchException extends RuntimeException {
    private final List<Throwable> errors = new ArrayList<>();

    public BatchException(String message) {
        super(message);
    }

    public void addError(Throwable t) {
        if (errors.isEmpty()) {
            // First exception - as suppressed to itself
            addSuppressed(t);
        } else {
            addSuppressed(t);
        }
        errors.add(t);
    }

    public int getErrorCount() {
        return errors.size();
    }
}

Custom Resource Management

If you write your own resource manager (thread pool, transaction manager) not using standard TWR:

public class CustomResourceManager implements AutoCloseable {
    private final List<Resource> resources = new ArrayList<>();
    private Throwable primaryException;

    public void close() {
        for (Resource r : resources) {
            try {
                r.close();
            } catch (Throwable t) {
                if (primaryException == null) {
                    primaryException = t;
                } else {
                    primaryException.addSuppressed(t);
                }
            }
        }
        if (primaryException != null) {
            throw new RuntimeException("Close failed", primaryException);
        }
    }
}

Diagnostics

  • Logging - Logback/Log4j2 automatically print suppressed with Suppressed: prefix
  • Unit tests - check getSuppressed() when testing resource cleanup
  • JSON Layout - suppressed as a separate array field in ELK
  • Metrics - count suppressed via Micrometer

Interview Cheat Sheet

Must know:

  • Suppressed exceptions - Java 7+ mechanism, allows one exception to carry a list of parallel errors
  • Appeared together with try-with-resources - for cases when both try and close() fail
  • In normal try-finally suppressed are NOT added automatically - exception from finally shadows try
  • addSuppressed() can be called manually for batch error processing
  • Self-suppression (e.addSuppressed(e)) -> IllegalArgumentException
  • Suppressed array is initialized lazily - only on first addSuppressed
  • Can be disabled: new Exception(msg, cause, false, true) - enableSuppression = false

Frequent follow-up questions:

  • How to get suppressed? - e.getSuppressed() returns Throwable[]
  • When do suppressed NOT work? - Manual try-finally, enableSuppression = false, only one exception
  • Why needed in batch processing? - Collect all errors of a batch in one BatchException
  • Is there memory overhead? - Yes, each suppressed = object + stack trace, dangerous for singletons

Red flags (NOT to say):

  • “Suppressed work in normal try-finally” - no, only in try-with-resources or manually
  • “Doesn’t matter how many suppressed to accumulate” - memory leak in long-lived objects
  • “Suppressed replace normal error handling” - no, it’s an infrastructure mechanism
  • “Serializing suppressed is free” - entire stack trace of each suppressed goes over network

Related topics:

  • [[22. What happens if an exception also occurs in the finally block]] - shadowing without suppressed
  • [[9. What is try-with-resources]] - main usage scenario
  • [[18. What is exception wrapping (wrapping)]] - cause vs suppressed
  • [[28. What is exception chaining]] - cause (reason) vs suppressed (parallel error)
  • [[11. What is the AutoCloseable interface]] - resources with suppressed on close