Большинство проблем с обработкой ошибок в 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