Что такое оборачивание (wrapping) исключений?
Chain of causality в логе будет выглядеть так:
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: — это переход к следующему звену цепочки.
Зачем оборачивать
- Добавить контекст:
"Failed to query users table for active users"понятнее, чем"Connection timeout" - Скрыть детали реализации: бизнес-логика не должна знать о
SQLException - Сохранить причину:
eпередаётся — стек-трейс оригинальной ошибки не теряется - Единая иерархия: все исключения сервиса — подтипы
ServiceException
Когда НЕ использовать обёртку
- Валидация входных данных —
IllegalArgumentExceptionне нуждается в обёртке. Это ошибка программирования, её нужно исправить, а не оборачивать - Программистские ошибки —
NullPointerException,IndexOutOfBoundsExceptionловить и оборачивать не нужно. Эти исключения указывают на баг — пусть падают до глобального обработчика - Слишком глубокая вложенность — если уже 5+ уровней
cause, не добавляйте ещё один без веской причины. Цепочка из 10 исключений = нечитаемый стек-трейс - Таймауты и retry — при автоматическом retry не оборачивайте каждую попытку, оборачивайте только финальный провал. Иначе получите:
RetryException→TimeoutException→TimeoutException→TimeoutException→SocketException - Performance-critical код — каждое обёрнутое исключение = новый объект +
fillInStackTrace(). В hot-path это заметные накладные расходы - «Оборачивание в обёртку» (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 logs —
causeкак отдельное поле для индексации
🎯 Шпаргалка для интервью
Обязательно знать:
- 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