Этап 12 - Обработка API ошибок через ControllerAdvice
Обработка ошибок становится заметной в первый момент, когда frontend-разработчик спрашивает: “Что именно мне показать пользователю, если этот request упал?” Если каждый controller возвращает ошибку в своем формате, frontend не может сделать один нормальный компонент ошибок. Один endpoint возвращает plain text, другой HTML error page, третий stack trace, а validation errors приходят вообще в другой форме. Backend технически может работать, но API получается неприятным и ненадежным.
В Spring есть более правильная модель: controllers описывают успешные HTTP-действия, services выбрасывают понятные exceptions, когда use case нельзя выполнить, а один global handler переводит эти exceptions в стабильный JSON error contract. Такой global handler обычно помечается @RestControllerAdvice. Он перехватывает exceptions из controllers и превращает их в ResponseEntity с правильным HTTP status и response body.

Последовательность request
Последовательность простая. Request приходит в OrderController. Controller вызывает orderService.cancel(orderId). Service загружает order, проверяет ownership и текущий status, затем выбрасывает OrderNotFoundException, AccessDeniedException или OrderAlreadyShippedException, если операция невозможна. Controller не ловит эти exceptions. Они уходят в advice class, где каждый тип exception получает понятный status и JSON body.
| Ситуация | HTTP status | Значение |
|---|---|---|
| Сломанный JSON или неверное поле | 400 Bad Request | Клиент отправил данные, которые API не может принять. |
| Order id не существует | 404 Not Found | Запрошенный ресурс отсутствует. |
| Order принадлежит другому user | 403 Forbidden | Пользователь известен, но не имеет доступа. |
| Order уже отправлен | 409 Conflict | Запрос конфликтует с текущим бизнес-состоянием. |
| Неожиданный баг или outage базы | 500 Internal Server Error | Сервер упал и не должен раскрывать внутренности. |
Конкретный Spring пример
public record ApiError(
String code,
String message,
List<FieldErrorDto> fieldErrors
) {}
@RestControllerAdvice
class ApiExceptionHandler {
@ExceptionHandler(OrderNotFoundException.class)
ResponseEntity<ApiError> notFound(OrderNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ApiError("ORDER_NOT_FOUND", ex.getMessage(), List.of()));
}
@ExceptionHandler(OrderAlreadyShippedException.class)
ResponseEntity<ApiError> conflict(OrderAlreadyShippedException ex) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(new ApiError("ORDER_ALREADY_SHIPPED", ex.getMessage(), List.of()));
}
}
Validation errors
Validation errors требуют еще один handler, потому что Spring выбрасывает MethodArgumentNotValidException до входа в тело controller method. В этом handler ты читаешь field errors и возвращаешь список вида field=quantity, message=must be greater than or equal to 1. Это намного полезнее, чем общий “Bad Request”.
Главное правило: error body является частью API contract. Не клади случайные имена exception classes в code. Не раскрывай SQL messages. Не возвращай 200 OK с полем error внутри JSON. Используй HTTP status для transport meaning, а stable code - для business meaning.
Частые ошибки
- Ловить exceptions в каждом controller и возвращать разные response shapes.
- Отдавать clients stack traces, SQL errors или имена Java exceptions.
- Использовать
500для business problems вроде “order already shipped”. - Возвращать
200 OKдля failed operations, потому что так проще написать controller.
Чеклист понимания
- Я могу связать типичные API failures с правильным HTTP status.
- Я могу объяснить, почему
@RestControllerAdviceнаходится вне отдельных controllers. - Я могу спроектировать стабильный JSON error body для frontend и integration clients.
Вопросы для самопроверки
- Почему service должен выбрасывать business exception, а не возвращать
null? - Что должен содержать validation error response?
- Почему stack trace опасен в public API response?