Вопрос 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+ уровней делают стек-трейс нечитаемым

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

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