Что такое exception chaining?
4. В signal-exceptions — лёгкие исключения-сигналы без контекста (FastException) 5. На границе микросервисов — передавайте DTO с кодом ошибки, не сериализуйте всю цепочку
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
- Валидация входных данных —
IllegalArgumentException("age must be positive")не нуждается в cause - Программистские ошибки —
NullPointerExceptionне оборачивают, их исправляют - Глубокая вложенность 10+ — стек-трейс становится нечитаемым, используйте truncation
- В signal-exceptions — лёгкие исключения-сигналы без контекста (
FastException) - На границе микросервисов — передавайте DTO с кодом ошибки, не сериализуйте всю цепочку
Middle Level
Поле cause в Throwable
Внутри Throwable есть поле private Throwable cause = this;.
Значение this по умолчанию означает, что причина ещё не установлена.
Метод initCause(Throwable) или конструктор super(message, cause) устанавливают это поле.
Ограничение: причину можно установить только один раз. Повторный initCause → IllegalStateException.
Архитектурные уровни цепочек
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/Splunk —
causeкак отдельное поле для индексации - Метрики — считайте глубину цепочки через
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: