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

Що таке 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 з кодом помилки замість серіалізації

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

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