JavaRush /Курси /JAVA 25 SELF /Ланцюжки винятків (Exception Chaining)

Ланцюжки винятків (Exception Chaining)

JAVA 25 SELF
Рівень 24 , Лекція 2
Відкрита

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();
}
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ