⚡ Розділ 7 · Питання #28

Чи можна перехопити і викинути виняток заново

Так, у Java можна повторно викинути перехоплений виняток за допомогою оператора throw e; всередині блоку catch.


🟢 Junior Level

Коротка відповідь для співбесіди (30 секунд)

Так, у Java можна повторно викинути перехоплений виняток за допомогою оператора throw e; всередині блоку catch.

Ця техніка називається Rethrowing і використовується тоді, коли поточному методу необхідно виконати локальну компенсуючу дію (відкотити транзакцію, звільнити блокування, зафіксувати метрику збою), але при цьому повідомити вищому коду, що операція завершилася невдачею.

Критично важливо: під час виконання throw e; стек-трейс об’єкта винятку не перезаписується — він зберігає точний рядок і кадри стека початкового виникнення збою.


Базовий приклад на Java 21

import java.sql.Connection;
import java.sql.SQLException;

public class TransactionService {

    public void transferFunds(Connection conn, long from, long to, double amount) throws SQLException {
        try {
            conn.setAutoCommit(false);
            debit(conn, from, amount);
            credit(conn, to, amount);
            conn.commit();
        } catch (SQLException e) {
            // 1. Виконуємо компенсуючу дію
            conn.rollback();
            // 2. Повторно викидаємо ТОЙ САМИЙ об'єкт винятку викликаючому коду
            throw e; 
        }
    }

    private void debit(Connection c, long acc, double amt) throws SQLException { }
    private void credit(Connection c, long acc, double amt) throws SQLException { }
}

🟡 Middle Level

Два підходи до повторного викидання: Pure Rethrow vs Wrapped Rethrow

                        Виняток у блоці catch
                                   │
         ┌─────────────────────────┴─────────────────────────┐
         ▼                                                   ▼
[ Pure Rethrow: throw e; ]                  [ Wrapped Rethrow: throw new CustomException(e); ]
- Зберігає вихідний об'єкт і клас           - Переводить помилку на новий рівень абстракції
- Стек-трейс незмінний                      - Збагачує помилку контекстом поточного шару
- Вимагає throws або перехоплення вище      - Створює ланцюжок причинності (Caused by:)

Механіка JVM та інструкція athrow

На рівні байткоду повторний кидок транслюється в стандартну інструкцію athrow:

  1. Віртуальна машина витягує посилання на вже існуючий у купі об’єкт винятку e.
  2. Інструкція athrow ініціює негайне розгортання стека викликів (Stack Unwinding) поточного потоку в пошуках наступного відповідного обробника у викликаючих методах.
  3. Оскільки метод fillInStackTrace() повторно не викликався, масив StackTraceElement[] залишається незмінним.

Precise Rethrow (Точний прокид у Java 7+)

До Java 7 при перехопленні загального предка catch (Exception e) компілятор змушував оголошувати throws Exception у заголовку методу, навіть якщо в try викидалися лише конкретні checked-винятки.

Починаючи з Java 7, компілятор javac виконує статичний аналіз потоку керування:

public class PreciseRethrowService {

    // Компілятор дозволяє точні типи, незважаючи на catch (Exception e)!
    public void execute() throws IOException, SQLException {
        try {
            readData(); // викидає IOException
            saveData(); // викидає SQLException
        } catch (Exception e) {
            recordFailureMetric(e);
            // Якщо e не переприсвоювалася (effectively final),
            // javac точно знає: вилетіти можуть ТІЛЬКИ IOException або SQLException!
            throw e; 
        }
    }

    private void readData() throws IOException { }
    private void saveData() throws SQLException { }
    private void recordFailureMetric(Exception e) { }
}

Антипатерни повторного викидання

1. Марний Rethrow (No-Op Catch)

// ❌ АНТИПАТЕРН: нульова цінність, зайвий код
try {
    service.doWork();
} catch (Exception e) {
    throw e; // Навіщо перехоплювати, якщо нічого не робиться?
}

2. Log and Rethrow (Залогуй і прокинь)

// ❌ АНТИПАТЕРН: дублювання логів
try {
    service.doWork();
} catch (Exception e) {
    log.error("Failed to do work", e); // Залогували тут...
    throw e;                           // ...і шар вище залогує цей самий виняток ще раз!
}

Правило: Якщо ви повторно викидаєте виняток, логувати його повинен найвищий шар застосунку (наприклад, @RestControllerAdvice у Spring).


🔴 Senior Level

Ризик витіснення помилки при збої компенсації (Compensation Failure)

Що станеться, якщо компенсуюча дія всередині catch сама завершиться помилкою?

// ❌ НЕБЕЗПЕКА: втрата первинного винятку e!
try {
    processPayment();
} catch (PaymentException e) {
    // Якщо rollback() упаде з SQLException...
    databaseConnection.rollback(); 
    // ...цей рядок НІКОЛИ НЕ ВИКОНАЄТЬСЯ! Первинний PaymentException стерто!
    throw e; 
}

Ідіоматичне рішення для Enterprise:

Компенсуючі виклики всередині блоку catch загортаються в захищений блок, а виникла вторинна помилка прикріплюється через addSuppressed():

try {
    processPayment();
} catch (PaymentException e) {
    try {
        databaseConnection.rollback();
    } catch (Exception rollbackEx) {
        // Зберігаємо помилку відкату як пригнічену, не втрачаючи первинний виняток!
        e.addSuppressed(rollbackEx);
    }
    throw e; // Гарантовано прокидаємо первинний виняток
}

Модифікація стека: метод fillInStackTrace()

У виняткових випадках (наприклад, під час реалізації асинхронних мостів або передачі помилок між потоками) розробнику потрібно, щоб стек-трейс відображав точку повторного викидання, а не точку оригінального створення:

try {
    runWorkerTask();
} catch (CustomException e) {
    // ПЕРЕЗАПИС СТЕКА: нативний виклик fillInStackTrace перезаписує кадри стека!
    // Оригінальне місце виникнення всередині runWorkerTask() буде стерто!
    throw (CustomException) e.fillInStackTrace();
}

Застереження: Цей прийом стирає оригінальний контекст помилки і повинен застосовуватися лише в спеціалізованому системному коді (наприклад, у диспетчерах реактивних потоків).


Вплив на Spring @Transactional

Повторний кидок безпосередньо впливає на транзакційну межу Spring:

  • Якщо з методу з @Transactional повторно викидається RuntimeException або Error, транзакція автоматично відкочується.
  • Якщо повторно викидається перевірюваний Exception, за замовчуванням транзакція НЕ відкотиться, якщо в анотації не вказано атрибут @Transactional(rollbackFor = Exception.class).

🎯 Шпаргалка для інтерв’ю

Питання для швидкої перевірки

  1. Чи перезаписується стек-трейс винятку під час виклику throw e; у блоці catch? Відповідь: Ні, стек-трейс не змінюється. Він зберігає інформацію про місце первинного виникнення збою.
  2. Як працює механізм Precise Rethrow у Java 7+? Відповідь: Компілятор javac відстежує типи винятків, які реально можуть бути викинуті в блоці try. Під час повторного throw e з блоку catch (Exception e) компілятор вимагає декларувати в throws лише конкретні типи вихідного блоку, а не широкий Exception.
  3. Що станеться, якщо в блоці catch перед рядком throw e; викличеться метод, який сам викине виняток? Відповідь: Новий виняток перерве виконання блоку catch, витіснить первинний виняток e і полетить угору по стеку. Первинний виняток e буде безповоротно втрачено, якщо вторинний виклик не був захищений блоком try-catch із викликом e.addSuppressed(...).
  4. Навіщо використовувати метод e.fillInStackTrace() при повторному викиданні винятку? Відповідь: Метод fillInStackTrace() примусово перезаписує стек викликів поточними фреймами на момент виклику. Це дозволяє приховати внутрішні фрейми реалізації або зафіксувати точку диспетчеризації в новому потоці, проте повністю стирає вихідне місце помилки.

Типові помилки та Red Flags

  • ❌ Твердження: “Під час виконання throw e; стек-трейс оновлюється поточним рядком”: Класичне нерозуміння будови Throwable. Стек перезаписується лише при явному виклику fillInStackTrace().
  • ❌ Практика “Log and Throw” (одночасне логування та прокидання): Породжує каскад однакових повідомлень про помилки в логах сервісу.
  • ❌ Незастраховані компенсуючі дії в catch: Падіння відкату транзакції затирає первинний бізнес-виняток.
  • ❌ Порожнє перехоплення і кидок catch (Exception e) { throw e; }: Надлишковий код, що не несе корисного навантаження.

Пов’язані теми