Managing Exception Handling

1. Creating Global Exception Handlers (@ControllerAdvice)

Example: Global exception handler with ProblemDetail

@RestControllerAdvice
public class GlobalExceptionHandler {
  @ExceptionHandler(EntityNotFoundException.class)
  public ResponseEntity<ProblemDetail> notFound(EntityNotFoundException ex) {
    var pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
    pd.setTitle("Resource not found");
    return ResponseEntity.status(HttpStatus.NOT_FOUND).body(pd);
  }
}
Annotation Scope
@ControllerAdvice Global to all controllers
@RestControllerAdvice Adds @ResponseBody implicitly
@ControllerAdvice(basePackages=…) Limit scope
@ControllerAdvice(annotations=RestController.class) Filter by annotation

2. Handling Specific Exceptions (@ExceptionHandler)

Form Effect
@ExceptionHandler(X.class) Single type
@ExceptionHandler({A.class, B.class}) Multiple types
Method on controller Local to that controller
Method on @ControllerAdvice Global

3. Returning Custom Error Responses

Example: Custom error response record

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

@ExceptionHandler(BusinessException.class)
ResponseEntity<ApiError> handle(BusinessException ex, HttpServletRequest req) {
  var body = new ApiError(400, ex.getCode(), ex.getMessage(),
                          Instant.now(), req.getRequestURI());
  return ResponseEntity.badRequest().body(body);
}

4. Using ResponseEntityExceptionHandler Base Class

Override Handles
handleMethodArgumentNotValid @Valid failures
handleHttpMessageNotReadable Malformed JSON
handleNoHandlerFoundException 404
handleHttpMediaTypeNotAcceptable 406
handleMissingServletRequestParameter 400 missing param

5. Handling Validation Exceptions

Example: Map validation errors to field messages

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String,Object>> onValidation(MethodArgumentNotValidException ex) {
  Map<String,String> fields = ex.getBindingResult().getFieldErrors().stream()
    .collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage,
                              (a,b) -> a));
  return ResponseEntity.badRequest()
    .body(Map.of("status", 400, "errors", fields));
}

6. Handling HTTP Status Exceptions

Pattern Use
throw new ResponseStatusException(NOT_FOUND, "id=" + id) Inline status throw
@ResponseStatus on custom exception Static mapping
ErrorResponseException RFC 7807 carrier

7. Customizing Error Attributes (ErrorAttributes bean)

Example: Inject trace ID into error attributes

@Component
public class TraceErrorAttributes extends DefaultErrorAttributes {
  @Override public Map<String,Object> getErrorAttributes(WebRequest req, ErrorAttributeOptions opts) {
    Map<String,Object> attrs = super.getErrorAttributes(req, opts);
    attrs.put("traceId", MDC.get("traceId"));
    return attrs;
  }
}

8. Configuring Error Pages (ErrorController)

Approach Description
error.html in templates/error/ Default fallback
404.html, 5xx.html Status-specific
Custom ErrorController Take over /error
server.error.whitelabel.enabled=false Disable default page

9. Logging Exceptions with Context Information

Example: Log unhandled exception with trace context

@ExceptionHandler(Exception.class)
ResponseEntity<ProblemDetail> unhandled(Exception ex, HttpServletRequest req) {
  String tid = MDC.get("traceId");
  log.error("Unhandled error tid={} path={} ", tid, req.getRequestURI(), ex);
  return ResponseEntity.internalServerError()
    .body(ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, "tid=" + tid));
}

10. Using Problem Details (RFC 7807)

Example: RFC 7807 ProblemDetail with type URI

@ExceptionHandler(OrderNotFoundException.class)
ProblemDetail handle(OrderNotFoundException ex) {
  ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
  pd.setType(URI.create("https://errors.example.com/orders/not-found"));
  pd.setTitle("Order Not Found");
  pd.setProperty("orderId", ex.getId());
  return pd;
}
Field Description
type URI identifying the problem
title Short summary
status HTTP status
detail Human-readable explanation
instance URI of the specific occurrence
Note: Enable globally via spring.mvc.problemdetails.enabled=true.

11. Handling Asynchronous Exception Handling

Context Mechanism
@Async void AsyncUncaughtExceptionHandler
@Async Future Caught on future.get()
DeferredResult setErrorResult(ex)
WebFlux onErrorResume / @ExceptionHandler still works