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