Исключения Java

Модель

Исключение прерывает обычный поток выполнения и переносит управление к подходящему обработчику. Объект исключения хранит тип, сообщение, причину и stack trace.

Throwable
├── Error
└── Exception
    ├── RuntimeException
    └── checked exceptions
  • Error сообщает о серьёзных проблемах среды/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.
  • Использовать исключение для обычного ветвления в горячем цикле.

Вопросы на собеседовании

  1. Чем checked-исключение отличается от unchecked?
  2. Что происходит при распространении исключения вверх по стеку?
  3. В каком порядке выполняются catch-блоки?
  4. Как try-with-resources обрабатывает две ошибки: из тела и из close?
  5. Почему важно сохранять cause?
  6. Как корректно обработать InterruptedException, если его нельзя пробросить?

Связанные заметки

Класс Object · Методы Java · Класс Optional · Сериализация · Кейс исключение было проглочено внутри транзакции