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 |