Skip to content

Spring Boot Exception Handling and Validation Patterns

As an application grows, it’s easy to accumulate many narrow @ExceptionHandler methods. Three ways to keep this manageable:

public class ApplicationException extends RuntimeException {}
@ExceptionHandler(ApplicationException.class)
public ResponseEntity<?> handleAppException(ApplicationException ex) {
return ResponseEntity.badRequest().body(ex.getMessage());
}
@ExceptionHandler({
IllegalArgumentException.class,
IllegalStateException.class
})
@ExceptionHandler(Exception.class)

Recommended hierarchy: business exceptions, then validation exceptions, then a generic fallback last.

Bean Validation checks structure and format, but sanitization is a related, distinct concern:

  • Bean Validation
  • Input trimming
  • Escape HTML
  • Regex validation
  • OWASP libraries for XSS protection
@Pattern(regexp = "^[a-zA-Z ]+$")
private String name;

Handle MethodArgumentNotValidException globally to return field-level error messages instead of a generic 400.

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationErrors(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return ResponseEntity.badRequest().body(errors);
}

Example response:

{
"name": "Name cannot be blank",
"email": "Invalid email"
}
@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface ValidPhone {
String message() default "Invalid phone number";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class PhoneValidator implements ConstraintValidator<ValidPhone, String> {
public boolean isValid(String value, ConstraintValidatorContext context) {
return value != null && value.matches("\\d{10}");
}
}
@ValidPhone
private String phoneNumber;
  • Group exception handlers around a common base exception (or list multiple exception types in one @ExceptionHandler) rather than letting narrow handlers proliferate; keep a generic Exception fallback last.
  • Sanitization (trimming, escaping, regex, XSS protection) is a separate concern from Bean Validation — validation checks shape, sanitization checks safety.
  • MethodArgumentNotValidException gives you BindingResult.getFieldErrors() to return field-level messages, which is far more useful to API clients than a generic 400.
  • A custom Bean Validation annotation needs three pieces: the annotation itself (@Constraint(validatedBy = ...)), a ConstraintValidator implementation, and applying the annotation to the field.