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

Що таке загортання (wrapping) винятків?

Chain of causality у лозі виглядатиме так:

Мовні версії: English Russian Ukrainian

Junior Level

Визначення

Загортання (wrapping) — це створення нового винятку, який містить оригінальний виняток як причину (cause).

Chain of causality (ланцюжок причин) — це послідовність винятків, де кожен наступний огортає попередній. Уявіть матрьошку: зовнішній виняток — це сервісний шар, всередині нього — виняток бази даних, а ще глибше — мережева помилка. Коли ви бачите stack trace з wrapping, JVM роздруковує весь ланцюжок від зовнішнього до найглибшого (root cause).

Приклад

try {
    // Код, який може викинути SQLException
    connection.executeQuery("SELECT * FROM users");
} catch (SQLException e) {
    // Огортаємо у свій виняток
    throw new DataAccessException("Failed to query users", e);
    //                                                           ^--- cause (причина)
}

Chain of causality у лозі виглядатиме так:

DataAccessException: Failed to query users
    at UserService.getUsers(UserService.java:25)
Caused by: SQLException: Connection timeout
    at DBConnection.executeQuery(DBConnection.java:42)

Рядок Caused by: — це перехід до наступної ланки ланцюжка.

Навіщо огортати

  1. Додати контекст: "Failed to query users table for active users" зрозуміліше, ніж "Connection timeout"
  2. Сховати деталі реалізації: бізнес-логіка не повинна знати про SQLException
  3. Зберегти причину: e передається — стек-трейс оригінальної помилки не втрачається
  4. Єдина ієрархія: усі винятки сервісу — підтипи ServiceException

Коли НЕ використовувати обгортку

  1. Валідація вхідних данихIllegalArgumentException не потребує обгортки. Це помилка програмування, її потрібно виправити, а не огортати
  2. Помилки програмістаNullPointerException, IndexOutOfBoundsException ловити та огортати не потрібно. Ці винятки вказують на баг — нехай падають до глобального обробника
  3. Занадто глибока вкладеність — якщо вже 5+ рівнів cause, не додавайте ще один без вагомої причини. Ланцюжок із 10 винятків = нечитабельний стек-трейс
  4. Таймаути та retry — при автоматичному retry не огортайте кожну спробу, огортайте тільки фінальний провал. Інакше отримаєте: RetryExceptionTimeoutExceptionTimeoutExceptionTimeoutExceptionSocketException
  5. Performance-critical код — кожен обгорнутий виняток = новий об’єкт + fillInStackTrace(). У hot-path це помітні накладні витрати
  6. «Загортання в обгортку» (wrap-a-wrap anti-pattern) — не огортайте виняток, який вже є обгорткою того самого рівня абстракції: ```java // ПОГАНО — ServiceException та DataAccessException на одному рівні, сенсу немає try { repo.findUser(id); } catch (DataAccessException e) { throw new ServiceException(“User lookup failed”, e); // DataAccessException вже достатньо абстрактний, огортати безглуздо }

// ДОБРЕ — огортаємо тільки при переході між рівнями абстракції // DAO шар (SQLException) → Service шар (DataAccessException) → Controller (ServiceException)

7. **Проброс без додавання контексту** — якщо обгортка не додає нового повідомлення або метаданих, це шум:
```java
// Безглуздо — повідомлення і причина ті самі
throw new MyException(e.getMessage(), e);

Як отримати причину

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

Middle Level

Як влаштовано у Throwable

Клас Throwable має поле private Throwable cause = this;.

При передачі в конструктор super(cause) викликається initCause().

Важливо: initCause() можна викликати тільки один раз. Повторний виклик — IllegalStateException.

Exception Translation у чистій архітектурі

Wrapping — це зміна рівня абстракції:

// Погано — клієнт залежить від Stripe
public interface PaymentService {
    void charge(Card card) throws StripeException;
}

// Добре — клієнт залежить від домену
public interface PaymentService {
    void charge(Card card) throws PaymentProviderException;
}

// Реалізація огортає
public class StripePaymentService implements PaymentService {
    public void charge(Card card) {
        try {
            stripeApi.charge(card);
        } catch (StripeException e) {
            throw new PaymentProviderException("Stripe charge failed", e);
        }
    }
}

Можна змінити Stripe на PayPal, не змінюючи сигнатури методів.

Запобігання втраті контексту

// ГИРШЕ — втрачається тип і стек-трейс причини
throw new MyException(e.getMessage());

// КРАЩЕ — передаємо об'єкт цілком
throw new MyException("Context: processing order " + orderId, e);

Senior Level

Stack Trace Depth

При глибокому загортанні (DB → Hibernate → Spring → Service → Controller) стек-трейс стає величезним. Це збільшує час формування та об’єм логів.

Рішення: на проміжних шарах використовувати «легкі» винятки без стек-трейсу, якщо оригінальна причина (cause) вже містить увесь необхідний стек.

Memory Overhead

Кожен об’єкт у ланцюжку cause — живе посилання. Якщо зберігати такі об’єкти в пам’яті (черга на обробку помилок) — затримка очищення пам’яті.

Circular References

Throwable захищений від нескінченних циклів:

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

Reflection та InvocationTargetException

При Method.invoke() будь-які винятки завжди огортаються в InvocationTargetException:

try {
    method.invoke(obj);
} catch (InvocationTargetException e) {
    Throwable realException = e.getCause(); // Справжня помилка
}

Unwrapping для налагодження

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

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

// Вручну
Throwable t = e;
while (t.getCause() != null) {
    t = t.getCause();
}

Custom Metadata при загортанні

Додавайте метадані, відомі на поточному шарі:

public class ServiceException extends RuntimeException {
    private final Long orderId;
    private final String action;

    public ServiceException(String message, Throwable cause, Long orderId, String action) {
        super(message, cause);
        this.orderId = orderId;
        this.action = action;
    }
}

Діагностика

  • Log Analysis — переконайтеся, що формат логів розкриває cause (%ex у Logback)
  • IntelliJ Debugger — розгорніть вузол cause і побачите все дерево помилок
  • JSON structured logscause як окреме поле для індексації

🎯 Шпаргалка для інтерв’ю

Обов’язково знати:

  • Wrapping — створення нового винятку з оригінальним як cause (матрьошка)
  • Конструктор super(message, cause) викликає initCause() — можна викликати тільки один раз
  • Ланцюжок причин (chain of causality) видно у логах через Caused by:
  • Wrapping змінює рівень абстракції: DAO → Service → Controller
  • Втрата контексту: new MyException(e.getMessage()) — погано, втрачається тип і стек-трейс
  • InvocationTargetException при рефлексії завжди огортає справжню помилку
  • Уникайте wrap-a-wrap anti-pattern — не огортайте обгортки того самого рівня

Часті уточнюючі запитання:

  • Чи можна викликати initCause() двічі? — Ні, буде IllegalStateException
  • Навіщо передавати cause, а не тільки e.getMessage()? — Щоб зберегти тип і стек-трейс оригінальної помилки
  • Коли НЕ огортати? — Валідація, помилки програміста (NPE), глибока вкладеність 5+ рівнів
  • Як отримати root cause вручну? — Цикл while (t.getCause() != null) t = t.getCause()

Червоні прапорці (НЕ говорити):

  • “Огортаю усі винятки без розбору” — це антипатерн
  • “Використовую e.getMessage() замість cause” — втрата типу та стек-трейсу
  • “Wrapping потрібен, щоб сховати помилку від викликаючого” — ні, щоб додати контекст
  • “Не важливо, скільки рівнів вкладеності” — 10+ рівнів роблять стек-трейс нечитабельним

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

  • [[29. Що таке chaining винятків]] — той самий механізм, інший ракурс
  • [[6. Що таке Throwable]] — поле cause та initCause()
  • [[17. Що робить метод printStackTrace()]] — як побачити ланцюжок у логах
  • [[16. Що таке stack trace]] — читання стек-трейсу з Caused by:
  • [[18. Як правильно логовати винятки]] — логування з %ex