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

Что такое exception chaining?

4. В signal-exceptions — лёгкие исключения-сигналы без контекста (FastException) 5. На границе микросервисов — передавайте DTO с кодом ошибки, не сериализуйте всю цепочку

Версии по языкам: English Russian Ukrainian

Junior Level

Определение

Exception chaining (цепочка исключений) — механизм, связывающий несколько исключений вместе, где каждое исключение указывает на своё cause (причину).

Пример

try {
    // Код, который может выбросить SQLException
    connection.executeQuery("SELECT * FROM users");
} catch (SQLException e) {
    // Оборачиваем в своё исключение, сохраняя причину
    throw new DataAccessException("Failed to query database", e);
    //                                              причина ↑
}

Как получить причину

try {
    service.process();
} catch (DataAccessException e) {
    Throwable cause = e.getCause(); // Оригинальное SQLException
    System.out.println("Root cause: " + cause.getMessage());
}

Зачем нужен

  • Сохранить стек-трейс — оригинальная ошибка не теряется
  • Добавить контекст — каждый слой добавляет свою информацию
  • Скрыть детали — клиент не зависит от SQLException

Когда НЕ использовать exception chaining

  1. Валидация входных данныхIllegalArgumentException("age must be positive") не нуждается в cause
  2. Программистские ошибкиNullPointerException не оборачивают, их исправляют
  3. Глубокая вложенность 10+ — стек-трейс становится нечитаемым, используйте truncation
  4. В signal-exceptions — лёгкие исключения-сигналы без контекста (FastException)
  5. На границе микросервисов — передавайте DTO с кодом ошибки, не сериализуйте всю цепочку

Middle Level

Поле cause в Throwable

Внутри Throwable есть поле private Throwable cause = this;.

Значение this по умолчанию означает, что причина ещё не установлена.

Метод initCause(Throwable) или конструктор super(message, cause) устанавливают это поле.

Ограничение: причину можно установить только один раз. Повторный initCauseIllegalStateException.

Архитектурные уровни цепочек

Layer Web:      catch (DataAccessException e) → 500 Internal Server Error
                     ↑
Layer Service:  catch (SQLException e) → throw new DataAccessException("...", e)
                     ↑
Layer DAO:      catch (SQLException e) → пробрасывает дальше
                     ↑
JDBC Driver:    throws SQLException

На каждом этапе сохраняется cause, чтобы в логах докопаться до реальной причины.

Поиск Root Cause

// Apache Commons Lang
Throwable root = ExceptionUtils.getRootCause(e);

// Guava
Throwable root = Throwables.getRootCause(e);

// Вручную
Throwable t = e;
while (t.getCause() != null) {
    t = t.getCause();
}
// t — корневая причина

Suppressed vs Cause

  • Cause — “почему я упал” (первопричина)
  • Suppressed — “что ещё пошло не так, пока я падал” (обычно при закрытии ресурсов)

Senior Level

Stack Trace Bloat

Глубокие цепочки (10+ уровней) создают огромные текстовые логи. Нагружают I/O и занимают место на диске.

Оптимизация: при логировании настраивайте лимиты глубины стека или используйте JSON логи, где стек-трейс — отдельное индексируемое поле.

Object Allocation и GC

Каждое звено цепочки — объект в куче. В Highload-системах частые ошибки с глубокими цепочками могут вызвать нагрузку на GC.

Circular Dependency

JVM предотвращает зацикливание цепочек:

e1.initCause(e2);
e2.initCause(e1); // IllegalArgumentException

Serialization

При передаче цепочки по сети (RMI/gRPC) все звенья должны быть сериализуемыми. Если одно из вложенных исключений — кастомный класс, которого нет у клиента — десериализация всей цепочки упадёт.

Решение: на границе микросервисов передавайте только DTO с сообщением и кодом ошибки.

Log Analysis

Убедитесь, что формат логов включает %ex (Logback) или аналогичный маркер, раскрывающий cause. Некоторые старые конфигурации пишут только сообщение верхнего исключения.

Диагностика

  • IntelliJ Debugger — разверните узел cause и увидите всё дерево ошибок
  • ELK/Splunkcause как отдельное поле для индексации
  • Метрики — считайте глубину цепочки через getSuppressed().length
  • Root Cause Tracking — каждое звено цепочки должно логироваться с Trace ID

🎯 Шпаргалка для интервью

Обязательно знать:

  • Exception chaining — механизм связывания исключений через cause (причину)
  • Throwable хранит private Throwable cause = this — устанавливается через конструктор или initCause()
  • Причину можно установить только один раз — повторный initCause()IllegalStateException
  • Каждый слой архитектуры добавляет свой контекст: DAO → Service → Controller
  • getCause() получает оригинальное исключение, getRootCause() — корневую причину
  • Cause vs Suppressed: cause = “почему я упал”, suppressed = “что ещё пошло не так пока я падал”
  • Глубокие цепочки (10+) создают Stack Trace Bloat — огромные логи, нагрузка на GC

Частые уточняющие вопросы:

  • Как найти root cause? — Цикл while (t.getCause() != null) t = t.getCause() или ExceptionUtils.getRootCause(e)
  • Чем cause отличается от suppressed? — Cause — первопричина, suppressed — параллельные ошибки (обычно при close)
  • Можно ли зациклить цепочку? — Нет, JVM предотвращает: e1.initCause(e2); e2.initCause(e1)IllegalArgumentException
  • Проблемы сериализации цепочки? — Все звенья должны быть Serializable; на границе микросервисов передавайте DTO

Красные флаги (НЕ говорить):

  • “Можно вызвать initCause() несколько раз” — только один раз
  • “Chaining и suppressed — одно и то же” — нет, cause = первопричина, suppressed = параллельные ошибки
  • “Цепочка из 20 исключений — это нормально” — Stack Trace Bloat, нагрузка на I/O и GC
  • “Сериализую всю цепочку через сеть” — DTO с кодом ошибки вместо сериализации

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

  • [[18. Что такое оборачивание (wrapping) исключений]] — создание цепочки через wrapping
  • [[23. Что такое suppressed exceptions]] — suppressed vs cause
  • [[15. Что такое stack trace]] — чтение стек-трейса цепочки
  • [[6. Что такое Throwable]] — поле cause и initCause()
  • [[16. Что делает метод printStackTrace()]] — печать всей цепочки с Caused by: