Логотип Workflow

Article

Updated at:

Stage 12: API Error Handling with ControllerAdvice

Stage 12 - API Error Handling with ControllerAdvice

Error handling becomes visible the first time a frontend developer asks: “What exactly should I show to the user when this request fails?” If every controller returns a different error shape, the frontend cannot build one normal error component. One endpoint returns plain text, another returns an HTML error page, the third returns a stack trace, and validation errors arrive in a completely different format. The backend may technically work, but the API is unpleasant and unreliable.

Spring gives a better model: controllers should describe successful HTTP actions, services should throw meaningful exceptions when a use case cannot be completed, and one global handler should translate those exceptions into a stable JSON error contract. That global handler is usually a class annotated with @RestControllerAdvice. It listens to exceptions thrown from controllers and turns them into ResponseEntity objects with the right HTTP status and response body.

Stage 12 - API Error Handling with ControllerAdvice

Request flow

The flow is simple. A request reaches OrderController. The controller calls orderService.cancel(orderId). The service loads the order, checks ownership and current status, and throws OrderNotFoundException, AccessDeniedException, or OrderAlreadyShippedException when the operation cannot continue. The controller does not catch these exceptions. They move to the advice class, where each exception type is mapped to a clear status and JSON body.

SituationHTTP statusMeaning
Broken JSON or invalid field400 Bad RequestThe client sent data the API cannot accept.
Order id does not exist404 Not FoundThe requested resource is absent.
Order belongs to another user403 ForbiddenThe user is known but not allowed to access it.
Order already shipped409 ConflictThe request conflicts with current business state.
Unexpected bug or database outage500 Internal Server ErrorThe server failed and should hide internals.

Concrete Spring example

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 need one more handler because Spring throws MethodArgumentNotValidException before the controller method body runs. In that handler you read field errors and return a list like field=quantity, message=must be greater than or equal to 1. This is much more useful than a generic “Bad Request”.

The important rule is that the error body is part of the API contract. Do not put random exception class names into code. Do not expose SQL messages. Do not return 200 OK with an error field inside the JSON. Use HTTP status for transport meaning and a stable code for business meaning.

Common mistakes

  • Catching exceptions in every controller and returning different response shapes.
  • Returning stack traces, SQL errors, or Java exception names to clients.
  • Using 500 for business problems such as “order already shipped”.
  • Returning 200 OK for failed operations because it is easier for the controller.

Understanding checklist

  • I can map common API failures to the right HTTP status.
  • I can explain why @RestControllerAdvice belongs outside individual controllers.
  • I can design a stable JSON error body for frontend and integration clients.

Self-check questions

  1. Why should the service throw a business exception instead of returning null?
  2. What should a validation error response contain?
  3. Why is a stack trace dangerous in a public API response?