← назад к разделу

Большинство проблем с обработкой ошибок в Java-приложениях сводятся к нескольким повторяющимся антипаттернам. Их легко допустить, но ещё легче не заметить — ошибка молча глотается, и отлаживать приходится уже на продакшне.

Проглоченное исключение — пустой catch

Самый опасный антипаттерн: catch перехватывает исключение и ничего с ним не делает. Программа продолжает работу как ни в чём не бывало, хотя что-то уже пошло не так.

// плохо
try {
    config = objectMapper.readValue(json, Config.class);
} catch (JsonProcessingException e) {
    // TODO: разобраться потом
}

После такого блока config остаётся null, и NullPointerException прилетит в совершенно другом месте. Связь с настоящей причиной потеряна.

Правило: в catch должно быть хотя бы одно из трёх — логирование, повторный выброс, значимое действие (fallback, метрика).

// хорошо
try {
    config = objectMapper.readValue(json, Config.class);
} catch (JsonProcessingException e) {
    throw new ConfigurationException("Не удалось разобрать конфигурацию", e);
}

Потеря стектрейса — исключение без cause

Когда вы оборачиваете одно исключение в другое, всегда передавайте оригинал как cause. Иначе вся цепочка вызовов, которая привела к ошибке, бесследно исчезает.

// плохо — теряем исходную причину
} catch (SQLException e) {
    throw new DataAccessException("Ошибка БД"); // e пропал
}

// хорошо — причина сохранена
} catch (SQLException e) {
    throw new DataAccessException("Ошибка БД", e);
}

В логах первый вариант покажет только DataAccessException без какого-либо контекста. Второй — полную цепочку с оригинальным SQLException, именем таблицы, кодом ошибки драйвера.

Исключения как управление потоком

Исключения — дорогой механизм: при создании JVM собирает стектрейс всех вызовов. Использовать их как goto — неправильно и медленно.

// плохо — исключение вместо if
try {
    int value = Integer.parseInt(input);
    return value;
} catch (NumberFormatException e) {
    return -1; // «не число» — это обычный случай, не ошибка
}

// хорошо — обычная проверка
if (input != null && input.matches("-?\\d+")) {
    return Integer.parseInt(input);
}
return -1;

Короткая формула: исключение — для исключительных ситуаций, которых в норме быть не должно. Если пользователь вводит «неправильные данные» — это ожидаемый случай, проверяйте его явно.

Проглоченный InterruptedException

InterruptedException — особый случай: его нельзя просто проглотить. Это сигнал потоку остановиться. Если проигнорировать, поток продолжит работу и никогда не завершится корректно.

// плохо — флаг прерывания потерян
try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    // ничего не делаем
}

// хорошо — восстанавливаем флаг
try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    throw new TaskInterruptedException("Задача прервана", e);
}

Если метод не может выбрасывать InterruptedException, минимум — восстановить флаг через Thread.currentThread().interrupt(), чтобы вызывающий код мог его прочитать.

Дублирование логов — log-and-throw

Антипаттерн «залогировал и выбросил» приводит к тому, что одна ошибка попадает в лог несколько раз: каждый слой перехватывает, логирует и бросает дальше.

// плохо — лог в каждом слое
} catch (SQLException e) {
    log.error("Ошибка в репозитории", e); // здесь
    throw new DataAccessException("Ошибка БД", e);
}

// ...выше по стеку...
} catch (DataAccessException e) {
    log.error("Ошибка в сервисе", e); // и здесь снова
    throw e;
}

В итоге один запрос порождает три одинаковых записи в логе — с разными сообщениями, но одним стектрейсом. Найти «настоящую» причину становится сложнее.

Правило: логируйте один раз — либо там, где обрабатываете и не перебрасываете, либо на верхнем уровне (@RestControllerAdvice). Промежуточные слои только оборачивают и бросают дальше.

Чек-лист правильной обработки

  • catch не пустой: логирование, повторный выброс или явное действие
  • оригинальное исключение всегда передаётся как cause
  • InterruptedException — восстанавливаем флаг Thread.currentThread().interrupt()
  • обычные случаи («неверный формат», «не найдено») — проверки, а не исключения
  • лог ошибки — один раз, на верхнем уровне; промежуточные слои не логируют то, что перебрасывают

Что почитать дальше

  • Модель ошибок и иерархия исключений — как выстраивать собственную иерархию исключений в приложении
  • Глобальная обработка ошибок — @RestControllerAdvice, Problem Details и единая точка логирования
  • Ошибки REST API и Problem Details — как правильно возвращать ошибки клиенту по RFC 9457