Suppressed Exceptions in Java

Naveen AnamaneniUpdated · 4 min read
mediumJava 25javaexceptions

Picture this: your code throws an exception while writing to a file, and then, while Java is closing that file, close() throws another exception. Which one should the caller see? The first one is the real problem; the second is a side effect. With an old-style finally, the second exception replaces the first, and the real cause is lost. Try-with-resources keeps the first as the main exception and attaches the others to it as suppressed exceptions.

Two Failures, Nothing Lost

Suppressed.java▶ Try it
public class Suppressed {
    static class Resource implements AutoCloseable {
        private final String name;
 
        Resource(String name) { this.name = name; }
 
        void use() {
            throw new IllegalStateException("use failed: " + name);
        }
 
        @Override
        public void close() {
            throw new IllegalArgumentException("close failed: " + name);
        }
    }
 
    public static void main(String[] args) {
        try (Resource a = new Resource("A");
             Resource b = new Resource("B")) {
            b.use();
        } catch (Exception e) {
            System.out.println("main: " + e.getMessage());
            for (Throwable s : e.getSuppressed()) {
                System.out.println("suppressed: " + s.getMessage());
            }
        }
    }
}
Output
main: use failed: B
suppressed: close failed: B
suppressed: close failed: A

How it works

  1. b.use() throws: this becomes the main exception.
  2. Java closes B, then A. Both close() calls throw too.
  3. Instead of replacing the main exception, each closing exception is added to it with addSuppressed.
  4. The caller catches the main exception, the real problem, and can still see the others with getSuppressed().
  5. Printed with printStackTrace(), the suppressed ones appear under a Suppressed: heading.

Why This Matters

Without suppression, debugging becomes guesswork: the stack trace says "close failed", which is a symptom, and the real failure has vanished. With suppression, the main exception tells you what went wrong first, and the suppressed list tells you what else broke on the way out.

SituationMain exceptionSuppressed
Body fails, closing succeedsBody's exceptionNone
Body succeeds, closing failsClosing's exceptionNone
Body fails, closing failsBody's exceptionClosing's exception(s)

Adding Your Own

Throwable.addSuppressed(other) is public, so you can use the same idea in your own cleanup code, for example when shutting down several services and wanting to report every failure, not just the first:

Shutdown.java▶ Try it
public class Shutdown {
    public static void main(String[] args) {
        IllegalStateException failure =
            new IllegalStateException("shutdown had problems");
        String[] services = {"cache", "mailer", "payments"};
        for (String s : services) {
            if (!s.equals("cache")) {
                failure.addSuppressed(new RuntimeException(s + " didn't stop"));
            }
        }
        System.out.println(failure.getMessage() + ": "
            + failure.getSuppressed().length + " problems");
        for (Throwable t : failure.getSuppressed()) {
            System.out.println("  " + t.getMessage());
        }
    }
}
Output
shutdown had problems: 2 problems
  mailer didn't stop
  payments didn't stop

Common Mistakes

Logging only getMessage(). You lose the suppressed exceptions and the cause.

Assuming the closing exception is the main one. When the body failed, the body's exception is the one you catch.

Writing cleanup in finally that can throw. It replaces the real exception; use try-with-resources or addSuppressed.

Interview Corner

What are suppressed exceptions?

They're exceptions that occur while another exception is already being thrown, typically when try-with-resources closes a resource after the body has failed. Rather than replacing the original exception, they're attached to it with addSuppressed and can be retrieved with getSuppressed(). This preserves the root problem while still recording secondary failures. The feature was added in Java 7.

Practice

  1. Easy: Change Resource.close() in Suppressed.java so it doesn't throw. What does the output become?
  2. Harder: Make use() succeed and only close() fail. Which exception is caught now, and are there any suppressed ones?

Quick check: Suppressed exceptions

4 questions
1The body throws X and close() throws Y. What is caught?
2How do you read suppressed exceptions?
3Only close() throws; the body succeeds. What is caught?
4Since which version do suppressed exceptions exist?

Key Takeaways

Was this tutorial helpful?

Lesson 225 of 482