Spring Boot - Global Exception Handling

Without a centralised exception handling strategy, each controller ends up with its own try-catch blocks producing inconsistent error formats. @ControllerAdvice lets you define exception handlers in a single class that apply automatically across all controllers.

Contents

@ControllerAdvice and @ExceptionHandler

@RestControllerAdvice combines @ControllerAdvice and @ResponseBody. Methods annotated with @ExceptionHandler are invoked whenever the specified exception escapes any controller method.

Java
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
        ErrorResponse body = new ErrorResponse(HttpStatus.NOT_FOUND.value(), ex.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneric(Exception ex) {
        ErrorResponse body = new ErrorResponse(
                HttpStatus.INTERNAL_SERVER_ERROR.value(), "An unexpected error occurred");
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(body);
    }
}

Custom exception classes

Java
public class ResourceNotFoundException extends RuntimeException {

    private final String resourceType;
    private final Long id;

    public ResourceNotFoundException(String resourceType, Long id) {
        super(resourceType + " with id " + id + " was not found");
        this.resourceType = resourceType;
        this.id = id;
    }

    public String getResourceType() { return resourceType; }
    public Long getId() { return id; }
}

Structured error response DTO

A consistent error body makes it easy for API consumers to parse errors programmatically.

Java
import java.time.Instant;

public class ErrorResponse {

    private final int status;
    private final String message;
    private final Instant timestamp;

    public ErrorResponse(int status, String message) {
        this.status = status;
        this.message = message;
        this.timestamp = Instant.now();
    }

    public int getStatus() { return status; }
    public String getMessage() { return message; }
    public Instant getTimestamp() { return timestamp; }
}

When a ResourceNotFoundException is thrown from any controller, the client receives:

JSON
{
  "status": 404,
  "message": "Product with id 99 was not found",
  "timestamp": "2026-03-10T08:15:30Z"
}

Handling validation errors

When a @RequestBody fails Bean Validation, Spring throws MethodArgumentNotValidException. Handle it to return readable field-level error messages.

Java
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.Map;
import java.util.stream.Collectors;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidation(
            MethodArgumentNotValidException ex) {

        Map<String, String> errors = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .collect(Collectors.toMap(
                        FieldError::getField,
                        fe -> fe.getDefaultMessage() == null ? "invalid" : fe.getDefaultMessage()
                ));
        return ResponseEntity.badRequest().body(errors);
    }
}