Исключения Java
Модель
Исключение прерывает обычный поток выполнения и переносит управление к подходящему обработчику. Объект исключения хранит тип, сообщение, причину и stack trace.
Throwable
├── Error
└── Exception
├── RuntimeException
└── checked exceptionsErrorсообщает о серьёзных проблемах среды/JVM; прикладной код обычно не пытается продолжать работу после них.- Checked-исключение — наследник
Exception, но неRuntimeException; его нужно поймать или объявить вthrows. - Unchecked-исключение —
RuntimeExceptionи его наследники; компилятор не требует явной обработки.
Checked/unchecked — правило компилятора, а не мера серьёзности ошибки.
throw и throws
throw выбрасывает конкретный объект:
if (amount.signum() <= 0) {
throw new IllegalArgumentException("amount must be positive");
}throws объявляет возможный выход метода:
String load(Path path) throws IOException {
return Files.readString(path);
}Исключение распространяется вверх по стеку, пока не найдётся совместимый catch. При раскрутке стека выполняются finally и закрытие ресурсов.
try / catch
try {
return parser.parse(input);
} catch (InvalidFormatException exception) {
metrics.incrementInvalidInput();
throw new ImportException("Cannot import document", exception);
}Обработчик должен делать одно из осмысленных действий:
- восстановить работу с известной семантикой;
- преобразовать ошибку на границе абстракций, сохранив cause;
- добавить полезный контекст и передать выше;
- завершить конкретную операцию.
Логировать и тут же пробрасывать одну ошибку на каждом слое не нужно: это создаёт дубликаты. Место логирования выбирают там, где есть контекст запроса и принято окончательное решение.
Несколько типов с одинаковой обработкой объединяются:
catch (IOException | SecurityException exception) {
throw new ImportException("Cannot read input", exception);
}Типы в multi-catch не могут находиться в отношении родитель/наследник. Обычные catch-блоки располагают от специфичного к общему.
finally
finally выполняется при обычном выходе, return и исключении, кроме аварийного завершения процесса/JVM и некоторых крайних случаев.
try {
work();
} finally {
releaseLocalState();
}Нельзя возвращать значение или выбрасывать новое исключение из finally без крайней необходимости: оно способно скрыть исходный результат или ошибку.
try-with-resources
Ресурс должен реализовать AutoCloseable:
try (InputStream input = Files.newInputStream(path);
BufferedInputStream buffered = new BufferedInputStream(input)) {
return buffered.readAllBytes();
}Ресурсы закрываются в обратном порядке. Если тело выбросило основное исключение, а close — ещё одно, основное сохраняется, а ошибка закрытия доступна через getSuppressed().
catch (IOException exception) {
for (Throwable suppressed : exception.getSuppressed()) {
logger.debug("Close failed", suppressed);
}
throw exception;
}Это безопаснее ручного finally, где ошибка закрытия легко заменяет исходную.
Собственные исключения
Тип исключения должен выражать ошибку уровня абстракции:
final class OrderNotFoundException extends RuntimeException {
OrderNotFoundException(long id) {
super("Order not found: " + id);
}
}Не включайте секреты и персональные данные в сообщение. Причину сохраняют:
throw new StorageException("Cannot load order " + id, sqlException);Checked-исключение уместно, если вызывающий реально может и должен выбрать восстановление. RuntimeException подходит для нарушения предусловий и ошибок, которые обрабатываются на дальней границе. Это проектное решение контракта, а не правило «I/O всегда checked».
InterruptedException
InterruptedException — сигнал отмены/прерывания потока. Его нельзя молча проглатывать:
try {
queue.put(task);
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
throw new TaskCancelledException("Interrupted", exception);
}Если метод может объявить throws InterruptedException, лучше передать сигнал выше. Если преобразует в другое исключение, обычно восстанавливает interrupt flag.
Исключения и транзакции
Если ошибка поймана внутри транзакционного метода и наружу не вышел сигнал об откате, инфраструктура может зафиксировать частичные изменения. Нужно понимать правила rollback конкретного фреймворка и не превращать ошибку в «успех» случайным catch. Практический разбор: Кейс исключение было проглочено внутри транзакции.
Ловушки
- Пустой
catchили возврат значения «по умолчанию», скрывающий сбой. - Ловить
Exception/Throwableслишком рано. - Терять cause при создании нового исключения.
- Логировать одну ошибку на каждом слое.
- Возвращать из
finally. - Игнорировать suppressed exceptions при диагностике ресурсов.
- Проглатывать
InterruptedException. - Использовать исключение для обычного ветвления в горячем цикле.
Вопросы на собеседовании
- Чем checked-исключение отличается от unchecked?
- Что происходит при распространении исключения вверх по стеку?
- В каком порядке выполняются catch-блоки?
- Как try-with-resources обрабатывает две ошибки: из тела и из
close? - Почему важно сохранять cause?
- Как корректно обработать
InterruptedException, если его нельзя пробросить?
Связанные заметки
Класс Object · Методы Java · Класс Optional · Сериализация · Кейс исключение было проглочено внутри транзакции