Що таке 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 з кодом помилки замість серіалізації
Пов’язані теми:
- [[19. Що таке загортання винятків]] — створення ланцюжка через wrapping
- [[24. Що таке suppressed винятки]] — suppressed vs cause
- [[16. Що таке stack trace]] — читання стек-трейсу ланцюжка
- [[6. Що таке Throwable]] — поле
causeтаinitCause() - [[17. Що робить метод printStackTrace()]] — друк всього ланцюжка з
Caused by: