Вопрос 24 · Раздел 7

Что такое suppressed exceptions?

Чаще всего — в 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 или реактивных стримах исключения из разных потоков не aggregируются через 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 летит по сети

Связанные темы:

  • [[22. Что произойдёт, если в блоке finally тоже возникнет исключение]] — затмение без suppressed
  • [[9. Что такое try-with-resources]] — основной сценарий использования
  • [[18. Что такое оборачивание (wrapping) исключений]] — cause vs suppressed
  • [[28. Что такое exception chaining]] — cause (причина) vs suppressed (параллельная ошибка)
  • [[11. Что такое AutoCloseable интерфейс]] — ресурсы с suppressed при закрытии