Spring Boot Cheatsheet

Exception Handling

Use this Spring Boot reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

@ControllerAdvice / @RestControllerAdvice

The recommended single-point exception handling for REST APIs.

@RestControllerAdvice   // = @ControllerAdvice + @ResponseBody
public class GlobalExceptionHandler {

    // Handle a specific custom exception
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ApiError> handleNotFound(ResourceNotFoundException ex,
                                                    HttpServletRequest request) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
            .body(new ApiError(404, "Not Found", ex.getMessage(),
                               Instant.now(), request.getRequestURI()));
    }

    // Handle validation errors from @RequestBody + @Valid
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiError> handleValidation(MethodArgumentNotValidException ex,
                                                      HttpServletRequest request) {
        String message = ex.getBindingResult().getFieldErrors().stream()
            .map(e -> e.getField() + ": " + e.getDefaultMessage())
            .collect(Collectors.joining(", "));
        return ResponseEntity.badRequest()
            .body(new ApiError(400, "Validation Failed", message,
                               Instant.now(), request.getRequestURI()));
    }

    // Handle constraint violations on @PathVariable / @RequestParam
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<ApiError> handleConstraint(ConstraintViolationException ex,
                                                      HttpServletRequest request) {
        String message = ex.getConstraintViolations().stream()
            .map(v -> v.getPropertyPath() + ": " + v.getMessage())
            .collect(Collectors.joining(", "));
        return ResponseEntity.badRequest()
            .body(new ApiError(400, "Constraint Violation", message,
                               Instant.now(), request.getRequestURI()));
    }

    // Catch-all — log and return 500
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiError> handleAll(Exception ex,
                                               HttpServletRequest request) {
        log.error("Unhandled exception", ex);
        return ResponseEntity.internalServerError()
            .body(new ApiError(500, "Internal Server Error",
                               "An unexpected error occurred",
                               Instant.now(), request.getRequestURI()));
    }
}

Standard Error Response Record

public record ApiError(
    int status,
    String error,
    String message,
    Instant timestamp,
    String path
) {}

Custom Exception Classes

// Base application exception
public class AppException extends RuntimeException {
    private final HttpStatus status;

    public AppException(String message, HttpStatus status) {
        super(message);
        this.status = status;
    }

    public AppException(String message, HttpStatus status, Throwable cause) {
        super(message, cause);
        this.status = status;
    }

    public HttpStatus getStatus() { return status; }
}

// Typed subclasses
public class ResourceNotFoundException extends AppException {
    public ResourceNotFoundException(String resource, Object id) {
        super("%s with id '%s' not found".formatted(resource, id), HttpStatus.NOT_FOUND);
    }
}

public class ConflictException extends AppException {
    public ConflictException(String message) {
        super(message, HttpStatus.CONFLICT);
    }
}

public class UnauthorizedException extends AppException {
    public UnauthorizedException(String message) {
        super(message, HttpStatus.UNAUTHORIZED);
    }
}

public class ForbiddenException extends AppException {
    public ForbiddenException(String message) {
        super(message, HttpStatus.FORBIDDEN);
    }
}
// In @ControllerAdvice — handle all AppExceptions with one handler
@ExceptionHandler(AppException.class)
public ResponseEntity<ApiError> handleApp(AppException ex, HttpServletRequest req) {
    return ResponseEntity.status(ex.getStatus())
        .body(new ApiError(ex.getStatus().value(),
                           ex.getStatus().getReasonPhrase(),
                           ex.getMessage(),
                           Instant.now(), req.getRequestURI()));
}

@ResponseStatus on Exception Classes

@ResponseStatus(HttpStatus.NOT_FOUND)   // applied when no @ExceptionHandler matches
public class ItemNotFoundException extends RuntimeException {
    public ItemNotFoundException(Long id) {
        super("Item " + id + " not found");
    }
}

@ControllerAdvice handlers take precedence over @ResponseStatus. Use @ResponseStatus only for simple cases with no custom response body.

Built-in Exceptions to Handle

ExceptionHTTP StatusTrigger
MethodArgumentNotValidException400@Valid fails on @RequestBody
ConstraintViolationException400@Validated fails on params
BindException400@ModelAttribute validation fails
HttpMessageNotReadableException400Malformed JSON body
MissingServletRequestParameterException400Required @RequestParam missing
MethodArgumentTypeMismatchException400Type coercion failure (e.g., string→int)
HttpRequestMethodNotSupportedException405Wrong HTTP method
HttpMediaTypeNotSupportedException415Wrong Content-Type
HttpMediaTypeNotAcceptableException406Accept header mismatch
NoResourceFoundException404No handler found (Spring 6.2+)
MaxUploadSizeExceededException413File upload exceeds limit
AccessDeniedException403Spring Security access denied
AuthenticationException401Spring Security auth failure

@ControllerAdvice Scoping

// Limit advice to specific packages or controllers
@ControllerAdvice(basePackages = "com.example.api")
@ControllerAdvice(assignableTypes = {UserController.class, OrderController.class})
@ControllerAdvice(annotations = RestController.class)

ResponseEntityExceptionHandler

Extend this abstract class to override Spring's default handling of standard MVC exceptions:

@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(
            MethodArgumentNotValidException ex,
            HttpHeaders headers, HttpStatusCode status, WebRequest request) {
        List<String> errors = ex.getBindingResult().getFieldErrors().stream()
            .map(e -> e.getField() + ": " + e.getDefaultMessage())
            .toList();
        ProblemDetail detail = ProblemDetail.forStatusAndDetail(status,
            "Validation failed: " + String.join(", ", errors));
        return ResponseEntity.badRequest().body(detail);
    }
}

RFC 7807 Problem Details (Spring 6+)

Spring Boot 3.x can return standard application/problem+json responses automatically.

spring.mvc.problemdetails.enabled=true
// Manually build a ProblemDetail
@ExceptionHandler(ResourceNotFoundException.class)
public ProblemDetail handleNotFound(ResourceNotFoundException ex) {
    ProblemDetail detail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND,
                                                             ex.getMessage());
    detail.setTitle("Resource Not Found");
    detail.setProperty("entityType", ex.getEntityType());
    return detail;
}

Logging in Exception Handlers

private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

@ExceptionHandler(Exception.class)
public ResponseEntity<ApiError> handleAll(Exception ex, HttpServletRequest request) {
    // Log with stack trace only for unexpected errors
    log.error("Unhandled exception on {} {}", request.getMethod(),
              request.getRequestURI(), ex);
    ...
}

@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiError> handleNotFound(ResourceNotFoundException ex) {
    log.debug("Resource not found: {}", ex.getMessage()); // debug — not an error
    ...
}

Common Pitfalls

  • Multiple @ExceptionHandler for the same type — only one @ControllerAdvice can claim a given exception type; duplicates throw an ambiguous mapping error at startup.
  • @ExceptionHandler in @Controller vs @ControllerAdvice — a handler inside a @Controller only catches exceptions from that controller; use @ControllerAdvice for global handling.
  • Order of @ControllerAdvice beans — use @Order or implement Ordered to control which advice applies first when multiple exist.
  • Spring Security exceptions before MVCAccessDeniedException and AuthenticationException are handled by the Security filter chain, not @ControllerAdvice; customize via AuthenticationEntryPoint and AccessDeniedHandler.
  • Returning the exception message in prod — sanitize messages in @ExceptionHandler to avoid leaking stack traces or internal details; use generic messages for 500s.
  • @ResponseBody missing — using @ControllerAdvice (not @RestControllerAdvice) without @ResponseBody on the handler returns a view name, not JSON.