Что произойдёт, если в блоке finally тоже возникнет исключение?
Если в finally возникнет исключение, оно вытеснит исключение из try. Оригинальная ошибка будет потеряна.
Junior Level
Основной эффект
Если в finally возникнет исключение, оно вытеснит исключение из try. Оригинальная ошибка будет потеряна.
try {
throw new RuntimeException("Original error");
} finally {
throw new RuntimeException("Finally error");
}
// Будет виден только "Finally error"
Почему это опасно
try {
// Критическая ошибка — БД недоступна
connection.executeQuery("SELECT * FROM users");
} finally {
// Ошибка при закрытии — NPE
connection.close(); // connection == null
}
// В логах только NPE, информация о БД потеряна
Вы будете чистить код от NPE, хотя реальная проблема — инфраструктура.
Как избежать
Используйте try-with-resources — он автоматически обрабатывает suppressed исключения:
try (Connection conn = dataSource.getConnection()) {
conn.executeQuery("SELECT * FROM users");
}
// Если и try, и close() упадут — оба исключения сохранятся
Когда НЕ полагаться на finally для очистки
- Код в finally может выбросить исключение — если
close(),disconnect(),release()могут упасть, оборачивайте каждый вызов в отдельныйtry-catchвнутри finally - Несколько ресурсов без TWR — если закрываете 3+ ресурса в одном finally, используйте отдельные try-catch для каждого, иначе первое исключение прервёт закрытие остальных
- Критические транзакции — в finally нельзя безопасно откатить транзакцию, если основное исключение уже произошло; используйте отдельный
catchблок для rollback - Async-контекст — в
CompletableFutureблок finally может выполниться в другом потоке, и исключение там потеряется без UncaughtExceptionHandler - Shutdown hooks — при
System.exit()finally может не выполниться вообще - Native resource cleanup — для JNI/native памяти используйте
Cleaner(Java 9+) вместо finally, так как native-ресурсы могут не освобождаться корректно
Middle Level
Механизм подавления (Shadowing) — пошагово
JVM несёт в стеке только один объект исключения как “активный” в каждый момент времени. Вот что происходит пошагово:
Шаг 1: Код в try выбрасывает исключение A (new RuntimeException("Original")).
Шаг 2: JVM помечает текущий фрейм стека как “выбрасывающий исключение” и сохраняет ссылку на A в специальном слоте фрейма.
Шаг 3: Перед выходом из метода JVM выполняет блок finally (это гарантируется спецификацией JLS 14.20.2).
Шаг 4: Код в finally выбрасывает исключение B (new RuntimeException("Finally")).
Шаг 5: JVM заменяет ссылку в слоте исключений фрейма с A на B. Ссылка на A теряется навсегда — нет способа получить её из catch или извне.
Шаг 6: Исключение B propagates вверх по стеку. A становится недостижимым для GC только после того, как весь стек раскрутится.
Визуально:
Фрейм стека (до finally): pendingException = A
Фрейм стека (после finally): pendingException = B // A потерян!
Конкретный пример с кодом
public class FinallyShadowing {
public static void main(String[] args) {
try {
System.out.println("Шаг 1: выбрасываем SQLException");
throw new SQLException("Database connection lost");
} finally {
System.out.println("Шаг 2: finally пытается закрыть ресурс");
// close() тоже падает — и это затмевает SQLException
throw new NullPointerException("Connection was null in finally");
}
}
}
Вывод в стек-трейсе:
Exception in thread "main" java.lang.NullPointerException: Connection was null in finally
at FinallyShadowing.main(FinallyShadowing.java:8)
Обратите внимание: SQLException полностью отсутствует в стек-трейсе. Вы будете отлаживать NPE, хотя настоящая проблема — потеря соединения с БД.
Как сохранить оригинальное исключение:
try {
throw new SQLException("Database connection lost");
} catch (Exception e) {
try {
// finally-логика отдельно
closeSafely();
} catch (Exception closeEx) {
e.addSuppressed(closeEx); // Сохраняем оба
}
throw e; // Оригинальное исключение propagates
}
Shadowing через return
finally может “проглотить” исключение даже без выброса нового:
public int dangerousMethod() {
try {
throw new RuntimeException("Error");
} finally {
return 42; // Исключение проглочено! Метод вернёт 42
}
}
Инструкция return в байт-коде finally завершает фрейм стека раньше, чем механизм обработки исключений успеет пробросить ошибку. SonarQube маркирует это как Blocker.
Современное решение: Try-with-resources
Начиная с Java 7:
- Если исключение в
try, а затем в автоматическомclose()— исключение изtryостаётся основным - Исключение из
close()добавляется как Suppressed - Полная картина:
Error A (Suppressed: Error B)
try (Resource r = new Resource()) {
r.doWork(); // IOException #1
} // close() бросает IOException #2
// #1 — основное, #2 — suppressed
catch (Exception e) {
System.out.println(e.getMessage()); // #1
System.out.println(e.getSuppressed()[0]); // #2
}
Senior Level
Resource Leaks при исключении в finally
Если исключение в finally произошло в середине очистки (закрытие первого из пяти сокетов), остальные ресурсы не будут закрыты — выполнение finally прервётся.
Senior Best Practice: оборачивайте каждый close() в отдельный try-catch внутри finally:
finally {
try { resource1.close(); } catch (Exception e) { log.warn("Failed to close 1", e); }
try { resource2.close(); } catch (Exception e) { log.warn("Failed to close 2", e); }
}
Или используйте try-with-resources — он делает это автоматически.
Analyzing Logs
Если видите в логах странные исключения из блоков очистки (SocketClosedException), всегда проверяйте, не скрывают ли они более ранние ошибки.
Debugger
При пошаговой отладке следите за переходом в finally. Если переменная исключения в кадре стека внезапно сменилась — вы поймали Shadowing.
Bytecode анализ
javap -c MyClass
Покажет, как return в finally перезаписывает значение возврата и как исключение затирает предыдущее.
Диагностика
getSuppressed()— проверяйте массив suppressed исключений- Static Analysis — Sonar предупреждает о risky finally блоках
- Log Correlation — связывайте исключения из
tryиfinallyпо Trace ID
🎯 Шпаргалка для интервью
Обязательно знать:
- Исключение из
finallyвытесняет исключение изtry— оригинальная ошибка потеряна навсегда - JVM хранит только один pending exception в слоте фрейма стека —
finallyперезаписывает его returnвfinallyтоже “проглатывает” исключение — метод вернёт значение вместо ошибки- Try-with-resources автоматически сохраняет оба исключения: основное + suppressed из
close() - Без TWR оборачивайте каждый
close()в отдельныйtry-catchвнутриfinally - При нескольких ресурсах в
finallyпервое исключение прервёт закрытие остальных — resource leak - В async-контексте исключение в
finallyможет потеряться безUncaughtExceptionHandler
Частые уточняющие вопросы:
- Как увидеть оригинальное исключение? — Без TWR никак — оно потеряно. Используйте try-with-resources
- Почему TWR лучше
finally? — Сохраняет оба исключения через suppressed механизм - Что если
finallyделаетreturn? — Исключение изtryпроглочено, метод вернёт значение - Как правильно закрывать ресурсы без TWR? — Каждый
close()в отдельномtry-catchвнутриfinally
Красные флаги (НЕ говорить):
- “В
finallyможно безопасно бросать исключения” — они затмят оригинальную ошибку - “Использую
finallyдля закрытия ресурсов” — устаревший подход, используйте TWR - “Return в
finally— нормальная практика” — SonarQube маркирует как Blocker - “Потерянное исключение не страшно” — вы будете отлаживать не ту проблему
Связанные темы:
- [[23. Что такое suppressed exceptions]] — как TWR сохраняет оба исключения
- [[9. Что такое try-with-resources]] — автоматическое управление ресурсами
- [[8. Гарантируется ли выполнение блока finally]] — когда finally может не выполниться
- [[19. Почему не стоит глотать исключения (catch empty)]] — потеря ошибки