Питання 24 · Розділ 7

Що таке suppressed винятки?

Найчастіше — у try-with-resources:

Мовні версії: English Russian Ukrainian

Junior Level

Визначення

Suppressed exceptions (придушені винятки) — це механізм Java 7+, який дозволяє одному винятку нести в собі список інших винятків, що сталися паралельно.

Де зустрічається

Найчастіше — у try-with-resources:

try (Resource r = new Resource()) {
    r.doWork(); // IOException #1 — основний
} // close() кидає IOException #2 — suppressed

Як отримати

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

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

Навіщо потрібен

До Java 7, якщо в try і в finally виникали винятки, другий затирав перший (shadowing). Розробники втрачали інформацію про кореневу причину помилки.

Чому саме Java 7: у Java 7 з’явився try-with-resources (JLS 14.20.3), який автоматично закриває ресурси. При автоматичному закритті через close() висока ймовірність, що close() викине виняток одночасно з винятком з основного try. Без suppressed механізму один виняток затирав би інший, роблячи налагодження неможливим. Suppressed exceptions — це інфраструктурне доповнення до TWR, а не самостійна фіча.

Коли suppressed НЕ додаються

  1. Ручний try-finally — у звичайному try { ... } finally { close(); } suppressed НЕ додаються автоматично. Виняток з finally затьмарює виняток з try. Suppressed працює тільки в try-with-resources або при ручному виклику addSuppressed().

  2. enableSuppression = false — якщо виняток створено з new Exception(msg, cause, false, true), виклики addSuppressed() ігноруватимуться.

  3. close() не кидає винятків — якщо ресурс закривається коректно, suppressed список буде порожнім. Це нормальний сценарій.

  4. Виняток тільки в одному місці — якщо впав тільки try (але не close()) або тільки close() (але не try), suppressed не буде — буде один звичайний виняток.

Коли НЕ покладатися на suppressed

  1. Ручний try-finally — suppressed НЕ працюють автоматично. Якщо ви пишете try { ... } finally { resource.close(); } і обидва можуть впасти — інформація буде втрачена. Використовуйте try-with-resources.

  2. Зовнішні бібліотеки без TWR — якщо стороння бібліотека не реалізує AutoCloseable або закриває ресурси вручну, suppressed не додадуться.

  3. Async-контекст — у CompletableFuture або реактивних стримах винятки з різних потоків не агрегуються через suppressed автоматично.

  4. Batch-обробка без патерну — якщо обробляєте пачку задач і кожна може впасти, suppressed не додадуться самі — потрібно вручну використовувати addSuppressed().

Коли НЕ використовувати suppressed exceptions

  1. Довгоживучі об’єкти-синглтони — accumulation suppressed винятків веде до memory leak
  2. Серіалізація через мережу — увесь список suppressed передається по мережі, збільшуючи payload
  3. Highload-системи — кожен suppressed виняток = об’єкт + стек-трейс у купі
  4. Як заміна нормальній обробці помилок — suppressed не замінюють try-catch на кожному ресурсі
  5. У бізнес-логіці — suppressed призначені для infrastructure коду (ресурси, batch-обробка)

Middle Level

Внутрішня реалізація

У Throwable є поле private Throwable[] suppressedExceptions (масив, а не список — для продуктивності).

  • Ініціалізується ліниво — тільки при першому виклику addSuppressed
  • Можна вимкнути при створенні: new Exception(msg, cause, false, true)enableSuppression = false

Як компілятор генерує код для try-with-resources

Коли ви пишете:

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

Компілятор розгортає це в (спрощено):

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(); // Немає основного винятку — close() летить як звичайний
        }
    }
}

Ключовий момент: компілятор зберігає посилання на основний виняток (primaryException) і при помилці close() додає його як suppressed до основного, а не замінює його.

Параметри конструктора Throwable

public Throwable(String message, Throwable cause,
                 boolean enableSuppression, boolean writableStackTrace)
  • enableSuppression — дозволяє/забороняє suppressed (true за замовчуванням)
  • writableStackTrace — якщо false, стек-трейс не заповнюється (прискорення створення винятку)

Роль у try-with-resources

Сценарій:

  1. Читаєте файл у tryIOException (диск відключився)
  2. TWR закриває файл → другий IOException (диска немає)
  3. У старій Java другий затер би перший
  4. У сучасній: перший летить до вас, другий — e.getSuppressed()

Ручне використання

Хоча TWR робить це автоматично, можна використовувати вручну:

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;
}

Обмеження

  • Self-Suppression: e.addSuppressed(e)IllegalArgumentException
  • Null Suppression: e.addSuppressed(null)NullPointerException
  • Order: придушені зберігаються в порядку додавання. У TWR — зворотний порядок оголошення ресурсів (LIFO)

Senior Level

Memory Leak Risk

Нескінченне додавання suppressed винятків до довгоживучого об’єкта (синглтон) — витік пам’яті. Кожен виняток тягне свій стек-трейс.

Serialization Cost

Увесь список suppressed винятків серіалізується разом з основним. У розподілених системах — величезні об’єми даних по мережі.

Batch Processing патерн

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()) {
            // Перший виняток — як suppressed до самого себе
            addSuppressed(t);
        } else {
            addSuppressed(t);
        }
        errors.add(t);
    }

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

Custom Resource Management

Якщо пишете свій менеджер ресурсів (пул потоків, транзакційний менеджер), що не використовує стандартний 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);
        }
    }
}

Діагностика

  • Logging — Logback/Log4j2 автоматично друкують suppressed з префіксом Suppressed:
  • Unit-тести — перевіряйте getSuppressed() при тестуванні очищення ресурсів
  • JSON Layout — suppressed як окреме масивне поле в ELK
  • Метрики — лічіть кількість suppressed через Micrometer

🎯 Шпаргалка для інтерв’ю

Обов’язково знати:

  • Suppressed exceptions — механізм Java 7+, дозволяє одному винятку нести список паралельних помилок
  • З’явилися разом з try-with-resources — для випадків, коли try та close() обидва падають
  • У звичайному try-finally suppressed НЕ додаються автоматично — виняток з finally затьмарить try
  • addSuppressed() можна викликати вручну для batch-обробки помилок
  • Self-suppression (e.addSuppressed(e)) → IllegalArgumentException
  • Suppressed масив ініціалізується ліниво — тільки при першому addSuppressed
  • Можна вимкнути: new Exception(msg, cause, false, true)enableSuppression = false

Часті уточнюючі запитання:

  • Як отримати suppressed?e.getSuppressed() повертає Throwable[]
  • Коли suppressed НЕ працюють? — Ручний try-finally, enableSuppression = false, тільки один виняток
  • Навіщо потрібні в batch-обробці? — Збирають усі помилки пачки в одному BatchException
  • Чи є memory overhead? — Так, кожен suppressed = об’єкт + стек-трейс, небезпечний для синглтонів

Червоні прапорці (НЕ говорити):

  • “Suppressed працюють у звичайному try-finally” — ні, тільки в try-with-resources або вручну
  • “Не важливо, скільки suppressed накопичувати” — memory leak у довгоживучих об’єктах
  • “Suppressed замінюють нормальну обробку помилок” — ні, це інфраструктурний механізм
  • “Серіалізація suppressed безкоштовна” — увесь стек-трейс кожного suppressed летить по мережі

Пов’язані теми:

  • [[23. Що станеться, якщо виняток виникне ще й в блоці finally]] — затьмарення без suppressed
  • [[9. Що таке try-with-resources]] — основний сценарій використання
  • [[19. Що таке загортання винятків]] — cause vs suppressed
  • [[29. Що таке chaining винятків]] — cause (причина) vs suppressed (паралельна помилка)
  • [[11. Що таке AutoCloseable інтерфейс]] — ресурси з suppressed при закритті