Какие исключения лучше использовать для бизнес-логики: проверяемые или непроверяемые и почему
Что проверяют
Знаете ли вы не только определения, но и то, как выбор типа исключения ведёт себя в реальном Spring-приложении: в транзакциях, лямбдах и обработчиках ошибок. Следом обычно просят показать, как такое исключение превращается в HTTP-ответ.
Как отвечать
Коротко: для бизнес-ошибок в современном Java-коде берут непроверяемые исключения, наследников RuntimeException, и объясняют почему:
- транзакции:
@Transactionalв Spring по умолчанию откатывает транзакцию только наRuntimeExceptionиError. Проверяемое исключение безrollbackForприведёт к коммиту половины операции; - лямбды и Stream API: функциональные интерфейсы вроде
Functionне объявляютthrows, поэтому checked-исключение внутриmapприходится заворачивать вручную; - слои:
throwsпротекает через сервис, фасад и контроллер, хотя обработать ошибку может только верхний уровень; - экосистема: Spring сам переводит
SQLExceptionв непроверяемую иерархиюDataAccessException.
Дальше покажите устройство: своя иерархия вроде BusinessException extends RuntimeException с кодом ошибки, конкретные наследники (InsufficientFundsException, OrderNotFoundException) и централизованная обработка в @RestControllerAdvice через @ExceptionHandler, который отдаёт ProblemDetail с нужным статусом.
Проверяемые исключения уместны, когда вызывающий код обязан и реально может отреагировать: публичный API библиотеки, повторная попытка при сбое внешнего ресурса. Для ожидаемых исходов вроде «пользователь не найден при поиске» часто лучше вообще обойтись без исключения и вернуть Optional или sealed-интерфейс с вариантами результата.
Где спотыкаются
Бросают проверяемое бизнес-исключение из метода с @Transactional и не знают, что транзакция закоммитится.
Используют исключения как обычное ветвление в горячем коде. Сбор стек-трейса стоит заметно дороже if, и если без исключения никак, у RuntimeException есть конструктор с флагом writableStackTrace.
Заворачивают исключение и теряют причину: throw new BusinessException(e.getMessage()) без передачи e вторым аргументом оставляет в логах только текст, без исходного стек-трейса.