1. Вступ
У цьому уроці ми розглянемо важливу техніку роботи з винятками — ланцюжки винятків (exception chaining). Ця техніка дає змогу не втрачати інформацію про першопричину помилки, навіть якщо ви «обгортаєте» один виняток іншим.
У реальних застосунках часто трапляється, що помилка виникає глибоко всередині стеку викликів — наприклад, під час роботи з базою даних, файловою системою або мережею. Припустімо, у вас є метод, який звертається до бази даних і може викинути SQLException. Але на рівні бізнес-логіки ви не хочете «засмічувати» код технічними деталями й надаєте перевагу викиданню власного винятку, наприклад, UserManagementException.
Що буде, якщо просто викинути новий виняток?
try {
// щось із базою даних
} catch (SQLException e) {
throw new UserManagementException("Помилка під час роботи з користувачами");
}
Проблема:
У цьому разі інформація про те, що саме сталося в базі даних (і стек викликів!), втрачається. У логах ви побачите лише UserManagementException, а що було причиною — невідомо.
2. Рішення: обгортання вихідного винятку (chaining)
Java дає змогу «обгорнути» один виняток іншим, передавши вихідний виняток як причину (cause) у конструктор нового винятку. Це й називається ланцюжком винятків.
Як це зробити?
Більшість стандартних і користувацьких винятків мають конструктор із другим параметром — Throwable cause:
public UserManagementException(String message, Throwable cause) {
super(message, cause);
}
Використання:
try {
// щось із базою даних
} catch (SQLException e) {
throw new UserManagementException("Помилка під час роботи з користувачами", e);
}
Тепер, якщо ви переглянете стек викликів (printStackTrace()), то побачите і свій виняток, і весь ланцюжок аж до самої першопричини!
3. Як отримати причину винятку
У будь-якого об’єкта типу Throwable є метод getCause(), який повертає вихідний виняток (або null, якщо його немає).
Приклад:
try {
// ...
} catch (UserManagementException e) {
Throwable cause = e.getCause();
if (cause != null) {
System.out.println("Першопричина: " + cause);
}
e.printStackTrace();
}
Навіщо це потрібно?
- Для налагодження: ви бачите не лише «що пішло не так» на верхньому рівні, а й де саме сталася помилка в глибині стеку.
- Для логування: можна записати у лог увесь ланцюжок помилок.
- Для передання інформації між рівнями застосунку: бізнес-рівень може «обгорнути» технічний виняток своїм, не втрачаючи деталей.
4. Приклад: ланцюжок винятків у реальному застосунку
Припустімо, у вас є метод, який завантажує користувача з бази даних:
public User loadUser(String username) throws UserManagementException {
try {
// Код, який може викинути SQLException
// ...
} catch (SQLException e) {
throw new UserManagementException("Не вдалося завантажити користувача: " + username, e);
}
}
Тут UserManagementException — ваш власний виняток:
public class UserManagementException extends Exception {
public UserManagementException(String message, Throwable cause) {
super(message, cause);
}
}
Що відбудеться у разі помилки?
- У логах буде видно і ваш виняток, і оригінальний SQLException з усіма деталями.
- За потреби можна отримати доступ до першопричини через getCause().
5. Як виглядає стек викликів зі ланцюжком винятків
Приклад виведення:
UserManagementException: Не вдалося завантажити користувача: vasya
at UserService.loadUser(UserService.java:15)
...
Caused by: java.sql.SQLException: Connection refused
at ...
Тут видно все: повний ланцюжок викликів — де виникла бізнес-помилка та який технічний виняток став причиною.
6. Практика: реалізуємо ланцюжок винятків
Крок 1. Створюємо власний виняток:
public class UserManagementException extends Exception {
public UserManagementException(String message) {
super(message);
}
public UserManagementException(String message, Throwable cause) {
super(message, cause);
}
}
Крок 2. Використовуємо ланцюжок:
try {
// щось небезпечне
} catch (SQLException e) {
throw new UserManagementException("Помилка під час роботи з БД", e);
}
Крок 3. Обробка на верхньому рівні:
На самому верхньому рівні програми перехоплюємо власний виняток і виводимо повідомлення про помилку разом із усім ланцюжком причин.
public class Main {
public static void main(String[] args) {
try {
runUserManagement();
} catch (UserManagementException e) {
System.err.println("Сталася помилка: " + e.getMessage());
// Виводимо ланцюжок причин
Throwable cause = e.getCause();
while (cause != null) {
System.err.println("Причина: " + cause.getMessage());
cause = cause.getCause();
}
}
}
private static void runUserManagement() throws UserManagementException {
try {
// імітація помилки БД
throw new SQLException("Немає з’єднання з БД");
} catch (SQLException e) {
throw new UserManagementException("Помилка під час роботи з БД", e);
}
}
}
7. Типові помилки під час роботи з ланцюжками винятків
Помилка № 1: викидаєте новий виняток без cause.
catch (SQLException e) {
throw new UserManagementException("Помилка", /* немає cause! */);
}
Погано: втрачається інформація про першопричину.
Помилка № 2: ви не реалізували конструктор із cause у власному винятку.
Якщо у вашому класі винятку немає конструктора із Throwable cause, ви не зможете передати причину — доведеться додати його вручну.
Помилка № 3: перехоплюєте й «глушите» виняток, не передаючи далі.
catch (SQLException e) {
// Просто логуємо й мовчимо
}
Погано: помилка «втрачається», програма продовжує працювати некоректно.
try {
userService.loadUser("vasya");
} catch (UserManagementException e) {
System.err.println("Помилка: " + e.getMessage());
if (e.getCause() != null) {
System.err.println("Першопричина: " + e.getCause());
}
e.printStackTrace();
}
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ