Що станеться, якщо в блоці 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 - “Втрачений виняток не страшний” — ви будете налагоджувати не ту проблему
Пов’язані теми:
- [[24. Що таке suppressed винятки]] — як TWR зберігає обидва винятки
- [[9. Що таке try-with-resources]] — автоматичне управління ресурсами
- [[8. Чи гарантується виконання блоку finally]] — коли finally може не виконатися
- [[20. Чому не варто ковтати винятки (порожній catch)]] — втрата помилки