Чому не варто ковтати винятки (порожній catch)?
Це перехоплення винятку без будь-яких дій:
Junior Level
Що таке «ковтання» винятків
Це перехоплення винятку без будь-яких дій:
try {
processOrder(order);
} catch (Exception e) {
// Порожньо — виняток «проковтнуто»
}
// Програма продовжує роботу наче нічого не сталося
Механізм: чому виняток втрачається
Коли JVM виконує catch блок, вона знімає виняток зі стека обробки помилок. Якщо блок catch порожній:
- JVM створює об’єкт винятку (це вже коштувало CPU-циклів на
fillInStackTrace()) - Заходить у
catchблок — там нічого немає - Продовжує виконання після
catch— об’єкт винятку стає недосяжним і буде зібраний GC - Жоден код вище по стеку не дізнається про помилку — метод, який викликав
processOrder(), думає, що все пройшло успішно
Це як дзвінок у двері, який ви почули, але вирішили не відкривати — той, хто дзвонив, пішов, а ви навіть не дізналися, хто це був.
Чому це погано
Коли виняток ковтається, програма переходить у неконсистентний стан:
try {
connection.beginTransaction();
connection.save(order); // SQLException!
connection.commit(); // Виконається — частина даних втрачена
} catch (SQLException e) {
// Порожньо — транзакція комітиться з неповними даними
}
Підсумок: частина даних записана, частина ні. Бізнес-логіка порушена. У логах — тиша.
Що робити замість цього
// Мінімум — залогувати
try {
processOrder(order);
} catch (Exception e) {
log.error("Failed to process order", e);
}
// Краще — пробросити далі
try {
processOrder(order);
} catch (Exception e) {
log.error("Failed to process order", e);
throw new OrderProcessingException("Order failed", e);
}
Middle Level
Коли це ДОПУСТИМО
Всього кілька сценаріїв, коли порожній (або майже порожній) catch block — це нормально:
1. Закриття ресурсів (до Java 7):
} finally {
try { if (socket != null) socket.close(); }
catch (IOException ignored) { /* Нічого не можна зробити — сокет все одно закриється */ }
}
Сучасне рішення: try-with-resources (він сам обробляє suppressed винятки).
2. Очікувані переривання при завершенні:
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// Відновлюємо статус переривання — обов'язково!
Thread.currentThread().interrupt();
}
Тут catch не порожній — ми відновлюємо interrupted flag.
3. Ignorable optional operations (наприклад, Optional.orElseGet патерн):
// Кешування — якщо кеш недоступний, обчислюємо напряму
try {
cache.put(key, value);
} catch (CacheException e) {
// Кеш — оптимізація, а не requirement. Його недоступність — не помилка бізнес-логіки.
// Мінімум — debug-лог:
log.debug("Cache unavailable, skipping", e);
}
4. Cleanup при shutdown:
try {
temporaryFile.delete();
} catch (IOException e) {
// Файл тимчасовий, ОС все одно почистить /tmp при перезавантаженні
log.debug("Failed to delete temp file {}", temporaryFile, e);
}
Ключовий принцип: якщо ви ігноруєте виняток, переконайтеся що:
- Це усвідомлене рішення, а не лінь
- Наслідки помилки не впливають на бізнес-логіку
- Є хоча б
log.debug()для налагодження
Коли НЕ використовувати порожній catch
- Бізнес-логіка — практично ніколи. Навіть
log.debug()краще за порожнечу, бо дає нитку для налагодження. Єдиний виняток: якщо бізнес-специфікація явно каже «ігнорувати цю помилку» — але тоді все одно логуйте на DEBUG - У транзакціях — проковтнутий
SQLExceptionпризведе до коміту часткових даних - У stream API —
nullу колекції замість помилки мовчки зламає downstream - В асинхронному коді —
CompletableFutureз проковтнутим винятком ніколи не завершиться exceptionally - У middleware — проковтнутий виняток поверне клієнту
200 OKзамість помилки
Hidden Latency
Ковтання винятків ховає проблеми продуктивності. Сервіс падає по таймауту, помилки ковтаються — ви бачите просто повільну роботу без розуміння причин.
CPU Spikes
Часте виникнення та ковтання винятків все одно навантажує CPU на fillInStackTrace(). «Тихий» виняток — не означає «безкоштовний».
Functional Streams
Ковтання всередині forEach або map — колекція з неповними даними без попередження:
list.stream()
.map(item -> {
try {
return process(item);
} catch (Exception e) {
// Проковтнули — результат null
return null;
}
})
.collect(Collectors.toList()); // [result1, null, result3, null]
Global Exception Handler
Якщо проковтнули виняток посередині ланцюжка, @ControllerAdvice у Spring поверне клієнту 200 OK — обман API.
Senior Level
Quiet Failure та зомбі-системи
Проковтнутий виняток перетворює систему на «зомбі» — працює, але некоректно. Без механізму самовідновлення це катастрофа.
Static Analysis
Правила SonarQube (“Exceptions should not be ignored”) та IntelliJ Inspections (“Empty catch block”) мають бути блокуючими в CI/CD.
Log at Least as DEBUG
Якщо виняток можна проігнорувати — логуйте хоча б на рівні DEBUG:
try {
optionalResource.close();
} catch (IOException e) {
log.debug("Failed to close optional resource", e);
}
Це врятує години налагодження.
Self-Healing Systems
Якщо ігноруєте помилку — має бути механізм самовідновлення:
try {
cache.put(key, value);
} catch (CacheException e) {
log.warn("Cache unavailable, falling back to direct computation", e);
return computeDirectly(); // Самовідновлення
}
Metric-driven detection
try {
processOrder(order);
} catch (Exception e) {
meterRegistry.counter("exceptions.swallowed", "type", e.getClass().getSimpleName()).increment();
log.debug("Swallowed exception", e);
}
Діагностика
- SonarQube — блокує порожні catch-блоки
- IntelliJ Inspections — підсвічує
catch (Exception e) {} - Prometheus/Grafana — алертинг на аномалії у swallowed exceptions
- Thread Dumps — якщо потік «завис» після проковтнутого винятку
🎯 Шпаргалка для інтерв’ю
Обов’язково знати:
- Ковтання винятків переводить програму в неконсистентний стан без запису до логів
- Порожній
catchвсе одно витрачає CPU наfillInStackTrace()— «тихий» виняток не безкоштовний - Допустимі сценарії: закриття ресурсів (до Java 7),
InterruptedExceptionз відновленням флага, опціональний кеш - Мінімум —
log.debug()навіть при усвідомленому ігноруванні - У транзакціях проковтнутий
SQLException= коміт часткових даних - В async-коді проковтнутий виняток =
CompletableFutureніколи не завершиться exceptionally - SonarQube та IntelliJ Inspections мають блокувати порожні catch-блоки в CI/CD
Часті уточнюючі запитання:
- Коли МОЖНА проковтнути виняток? — Опціональні операції (кеш), cleanup при shutdown, але мінімум з
log.debug() - Що робити з
InterruptedException? — Відновити флаг:Thread.currentThread().interrupt() - Чому в транзакціях це катастрофа? — Проковтнута помилка = частковий коміт, дані пошкоджені
- Як виявити проблему в production? — SonarQube rules, Prometheus метрики swallowed exceptions, thread dumps
Червоні прапорці (НЕ говорити):
- “У мене всюди порожні catch, якщо помилка не критична” — це зомбі-система
- “Ковтаю винятки в бізнес-логіці” — ніколи не допустимо
- “Порожній catch не впливає на продуктивність” —
fillInStackTrace()коштує CPU-циклів - “Мені не потрібні логи, я знаю що помилка трапляється” — без логів немає налагодження
Пов’язані теми:
- [[18. Як правильно логовати винятки]] — логування щонайменше на DEBUG
- [[24. Що таке suppressed винятки]] — TWR зберігає обидві помилки
- [[23. Що станеться, якщо виняток виникне ще й в блоці finally]] — втрата помилки в finally
- [[25. Чи можна мати кілька catch блоків для одного try]] — правильна обробка різних типів