Вопрос 28 · Раздел 7

Можно ли повторно бросить исключение?

Повторный бросок (rethrow) — перехват исключения в catch с целью совершить действие (логирование) и отправка дальше:

Версии по языкам: English Russian Ukrainian

Junior Level

Да, можно!

Повторный бросок (rethrow) — перехват исключения в catch с целью совершить действие (логирование) и отправка дальше:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class OrderService {
    private static final Logger log = LoggerFactory.getLogger(OrderService.class);

    public void processOrder(Long orderId) {
        try {
            orderRepository.save(orderId);
        } catch (Exception e) {
            log.error("Failed to process order id={}", orderId, e); // Логируем
            throw e; // Пробрасываем дальше
        }
    }
}

Зачем это нужно

  • Логирование — записать ошибку перед тем, как она уйдёт дальше
  • Метрики — посчитать количество ошибок
  • Очистка — освободить ресурсы перед пробросом
try {
    connection.beginTransaction();
    connection.save(order);
} catch (SQLException e) {
    connection.rollback(); // Откатываем транзакцию
    throw e; // Пробрасываем дальше
}

Обёртка при повторном броске

Часто оборачивают в другое исключение:

try {
    repository.save(order);
} catch (SQLException e) {
    throw new OrderException("Failed to save order id=" + order.getId(), e); // Новый тип + контекст
}

Когда НЕ использовать rethrow

  1. Без добавления ценностиcatch (Exception e) { throw e; } бесполезен без логирования/метрик
  2. Дублирование логирования — если исключение уже залогировано на этом уровне, не логируйте повторно при rethrow
  3. Вместо оборачивания — если нужен новый тип исключения, используйте wrapping с cause, а не голый rethrow
  4. В транзакциях без rollback — rethrow unchecked исключения в @Transactional откатит транзакцию, checked — нет
  5. Многократный rethrow — не ловите/бросайте одно и то же исключение на каждом слое — достаточно одного уровня

Middle Level

Инструкция athrow

На уровне байт-кода повторный бросок — обычная инструкция athrow. Она берёт объект исключения из стека операндов и ищет обработчик в Exception Table.

Важно: при throw e; стек-трейс не меняется. В нём будет строка оригинального возникновения, а не строка throw e.

Precise Rethrow (Java 7+)

Одна из самых полезных, но редко обсуждаемых функций:

public void process() throws IOException, SQLException {
    try {
        if (condition) throw new IOException();
        else throw new SQLException();
    } catch (Exception e) { // Ловим общий предок
        log.error("Intermediary processing");
        throw e; // Компилятор "знает" — только IOException или SQLException
    }
}

До Java 7 пришлось бы объявить throws Exception. Теперь компилятор анализирует содержимое try.

Rethrow с оборачиванием (Exception Chaining)

// Плюс: добавляем контекст текущего слоя
throw new MyException("Context: processing order " + orderId, e);

// Минус: новый объект + новый fillInStackTrace()
// Всегда передавайте оригинальное исключение как cause!

Side Effects в rethrow

В высоконагруженных системах rethrow используется для метрик:

catch (RuntimeException e) {
    metrics.increment("errors.count");
    throw e;
}

Senior Level

Double Traversal

Повторный бросок заставляет JVM заново сканировать Exception Table. В глубоких иерархиях частые rethrow создают небольшую нагрузку на CPU.

Checked to Unchecked

Часто checked исключения пробрасываются как unchecked — “разрывает” обязательства в сигнатуре:

try {
    // checked exception
    Files.readAllLines(path);
} catch (IOException e) {
    throw new RuntimeException(e); // Unchecked — метод больше не объявляет throws
}

Sneaky Throws

Трюк с пробросом checked исключения без объявления в throws:

@SuppressWarnings("unchecked")
private static <E extends Throwable> void sneakyThrow(Throwable e) throws E {
    throw (E) e;
}

Lombok @SneakyThrows использует этот же механизм.

Losing Exceptions

Если в блоке catch вызвали метод, который сам бросил исключение, и не пробросили оригинал — оригинал потерян:

catch (Exception e) {
    log.info("Error");        // OK
    doSomethingThatAlsoThrows(); // Новое исключение! Оригинал e потерян!
    throw e; // Никогда не выполнится
}

fillInStackTrace() — обновление стека

Если хотите обновить стек-трейс текущей строкой (чтобы в логах было видно место rethrow):

throw (RuntimeException) e.fillInStackTrace();

Делайте это только если понимаете зачем — оригинальный стек-трейс будет потерян.

Диагностика

  • Log Analysis — убедитесь, что каждый rethrow логируется с контекстом
  • Метрики — считайте rethrow через Micrometer
  • Debugger — в IntelliJ разверните узел cause для просмотра всей цепочки
  • javap -c — покажет инструкцию athrow в байт-коде

🎯 Шпаргалка для интервью

Обязательно знать:

  • Rethrow — перехват исключения в catch с действием (лог, метрики, rollback) и проброс дальше через throw e
  • При throw e; стек-трейс не меняется — показывает оригинальное место возникновения
  • Checked исключения можно пробросить без потери типизации благодаря Precise Rethrow (Java 7+)
  • Обёртка при rethrow добавляет контекст: throw new OrderException("msg", e) — всегда передавайте cause
  • Rethrow unchecked в @Transactional откатит транзакцию, checked — нет
  • Бесполезный rethrow: catch (Exception e) { throw e; } без логирования/метрик/очистки
  • e.fillInStackTrace() обновляет стек-трейс — используйте осознанно, оригинальный стек будет потерян

Частые уточняющие вопросы:

  • Меняется ли стек-трейс при throw e? — Нет, показывает оригинальное место ошибки
  • Что такое Precise Rethrow? — Java 7+: компилятор анализирует try и сохраняет точные типы в throws
  • Когда rethrow бесполезен? — Без логирования, метрик, очистки — голый catch { throw e; }
  • Как обновить стек-трейс?e.fillInStackTrace() — но оригинальный стек будет потерян

Красные флаги (НЕ говорить):

  • “При rethrow стек-трейс обновляется” — нет, только при fillInStackTrace()
  • “Rethrow checked исключения не откатывает @Transactional” — зависит от типа: unchecked откатит, checked — нет
  • “Многократный rethrow на каждом слое — хорошая практика” — достаточно одного уровня
  • “Rethrow без cause теряет оригинальную ошибку” — при throw e без обёртки ошибка не теряется

Связанные темы:

  • [[18. Что такое оборачивание (wrapping) исключений]] — обёртка при rethrow
  • [[20. Что делает ключевое слово throws]] — Precise Rethrow сохраняет типы
  • [[28. Что такое exception chaining]] — chaining при обёртке с cause
  • [[21. Можно ли пробросить checked exception из метода без throws]] — sneaky throws альтернатива
  • [[17. Как правильно логировать исключения]] — логирование перед rethrow