Вопрос 20 · Раздел 7

Почему не стоит глотать исключения (catch empty)?

Это перехват исключения без каких-либо действий:

Версии по языкам: 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-циклов
  • “Мне не нужны логи, я знаю что ошибка случается” — без логов нет отладки

Связанные темы:

  • [[17. Как правильно логировать исключения]] — логирование как минимум на DEBUG
  • [[23. Что такое suppressed exceptions]] — TWR сохраняет обе ошибки
  • [[22. Что произойдёт, если в блоке finally тоже возникнет исключение.md]] — потеря ошибки в finally
  • [[24. Можно ли несколько catch блоков для одного try]] — правильная обработка разных типов