Питання 20 · Розділ 7

Чому не варто ковтати винятки (порожній catch)?

Це перехоплення винятку без будь-яких дій:

Мовні версії: English Russian Ukrainian

Junior Level

Що таке «ковтання» винятків

Це перехоплення винятку без будь-яких дій:

try {
    processOrder(order);
} catch (Exception e) {
    // Порожньо — виняток «проковтнуто»
}
// Програма продовжує роботу наче нічого не сталося

Механізм: чому виняток втрачається

Коли JVM виконує catch блок, вона знімає виняток зі стека обробки помилок. Якщо блок catch порожній:

  1. JVM створює об’єкт винятку (це вже коштувало CPU-циклів на fillInStackTrace())
  2. Заходить у catch блок — там нічого немає
  3. Продовжує виконання після catch — об’єкт винятку стає недосяжним і буде зібраний GC
  4. Жоден код вище по стеку не дізнається про помилку — метод, який викликав 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

  1. Бізнес-логіка — практично ніколи. Навіть log.debug() краще за порожнечу, бо дає нитку для налагодження. Єдиний виняток: якщо бізнес-специфікація явно каже «ігнорувати цю помилку» — але тоді все одно логуйте на DEBUG
  2. У транзакціях — проковтнутий SQLException призведе до коміту часткових даних
  3. У stream APInull у колекції замість помилки мовчки зламає downstream
  4. В асинхронному кодіCompletableFuture з проковтнутим винятком ніколи не завершиться exceptionally
  5. У 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]] — правильна обробка різних типів