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();
}
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ