Home/Learn/Spring Boot/Exception Handling — @ControllerAdvice

Exception Handling — @ControllerAdvice

Intermediate
Web / REST

@ControllerAdvice combined with @ExceptionHandler centralises error handling across all controllers, returning consistent error response DTOs.

Overview

Without centralised exception handling, every controller would need its own try-catch blocks and ad-hoc error responses — leading to inconsistent HTTP status codes, leaked stack traces, and duplicated code. @ControllerAdvice (or @RestControllerAdvice, which adds @ResponseBody) is a cross-cutting component that intercepts exceptions thrown by any @Controller in the application and maps them to well-structured error responses. Combined with @ExceptionHandler methods, you get a single place to define every error scenario: validation failures return 400 with field-level detail, entity-not-found returns 404, and unexpected exceptions return 500 — all with a consistent JSON shape that your API clients can rely on.

Building a Global Exception Handler

Annotate a class with @RestControllerAdvice to make it the single source of truth for error handling. Each @ExceptionHandler method declares which exception type(s) it handles. The return type is your error DTO — keep it consistent across all handlers so clients parse one shape. Use @ResponseStatus or ResponseEntity to control the HTTP status code.

Java — Spring Boot
// Consistent error response DTO
@Getter
@Builder
public class ApiError {
    private int     status;
    private String  error;
    private String  message;
    private Instant timestamp;
}

// Global exception handler
@RestControllerAdvice
public class GlobalExceptionHandler {

    // 404 — Resource not found
    @ExceptionHandler(EntityNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ApiError handleNotFound(EntityNotFoundException ex) {
        return ApiError.builder()
            .status(404)
            .error("Not Found")
            .message(ex.getMessage())
            .timestamp(Instant.now())
            .build();
    }

    // 409 — Business rule violation
    @ExceptionHandler(DuplicateResourceException.class)
    @ResponseStatus(HttpStatus.CONFLICT)
    public ApiError handleConflict(DuplicateResourceException ex) {
        return ApiError.builder()
            .status(409).error("Conflict").message(ex.getMessage())
            .timestamp(Instant.now()).build();
    }

    // 500 — Catch-all for unexpected errors
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ApiError handleAll(Exception ex) {
        log.error("Unhandled exception", ex);
        return ApiError.builder()
            .status(500).error("Internal Server Error")
            .message("An unexpected error occurred")
            .timestamp(Instant.now()).build();
    }
}

Handling Bean Validation Errors

When @Valid fails on a @RequestBody parameter, Spring throws MethodArgumentNotValidException. Handle it in @RestControllerAdvice to extract per-field error messages and return them in the 400 response body. This gives API consumers a clear map of which fields failed and why.

Java — Spring Boot
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiValidationError handleValidation(MethodArgumentNotValidException ex) {
    Map<String, String> fieldErrors = ex.getBindingResult()
        .getFieldErrors()
        .stream()
        .collect(Collectors.toMap(
            FieldError::getField,
            fe -> fe.getDefaultMessage() != null ? fe.getDefaultMessage() : "Invalid value",
            (a, b) -> a  // keep first message if multiple violations on same field
        ));

    return ApiValidationError.builder()
        .status(400)
        .error("Validation Failed")
        .fieldErrors(fieldErrors)   // e.g. {"email": "must be a valid email address"}
        .timestamp(Instant.now())
        .build();
}

// Also handle path variable / query param type mismatches
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiError handleTypeMismatch(MethodArgumentTypeMismatchException ex) {
    return ApiError.builder()
        .status(400).error("Bad Request")
        .message("Invalid value for parameter: " + ex.getName())
        .timestamp(Instant.now()).build();
}

Custom Business Exceptions

Define a hierarchy of application-specific unchecked exceptions. Throw them from service layer; let @RestControllerAdvice handle the translation to HTTP. This keeps service layer code clean (no HttpStatus references) and makes the mapping explicit and testable.

Java — Service Layer
// Custom exception hierarchy
public class ApplicationException extends RuntimeException {
    public ApplicationException(String message) { super(message); }
}

public class EntityNotFoundException extends ApplicationException {
    public EntityNotFoundException(String entity, Object id) {
        super(entity + " not found with id: " + id);
    }
}

public class InsufficientStockException extends ApplicationException {
    public InsufficientStockException(Long productId, int available) {
        super("Product " + productId + " has only " + available + " units in stock");
    }
}

// Service — throws business exception, no HTTP knowledge
@Service
public class OrderService {
    public Order getOrder(Long id) {
        return orderRepo.findById(id)
            .orElseThrow(() -> new EntityNotFoundException("Order", id));
    }
}

Key Points to Remember

  • 1@RestControllerAdvice centralises all exception-to-response mapping in one class, eliminating try-catch in every controller.
  • 2Return a consistent error DTO (status, error, message, timestamp) so all API consumers can parse errors uniformly.
  • 3MethodArgumentNotValidException carries per-field validation errors — extract and return field-level messages in a 400 response.
  • 4Define a custom exception hierarchy in the service layer; never reference HttpStatus there — keep HTTP concerns in the advice class.
  • 5Always have a catch-all @ExceptionHandler(Exception.class) as the last resort to prevent stack traces leaking to clients.
  • 6@ResponseStatus on the method is convenient; use ResponseEntity<ApiError> when you need to set response headers dynamically.

Interview Questions

Sign in to ask Aria
1

How do you implement global exception handling in a Spring Boot REST application?

MediumAmazon
2

What is the difference between @ControllerAdvice and @RestControllerAdvice?

EasyInfosys
3

How do you return field-level validation errors in a Spring Boot API when @Valid fails?

MediumFlipkart
4

Where should you throw exceptions — in the controller or service layer? Why?

MediumThoughtworks
5

How do you prevent a Spring Boot application from leaking internal stack traces to API clients?

EasyGoogle

Ask Aria about Exception Handling — @ControllerAdvice

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…