@ControllerAdvice and @ExceptionHandler — return consistent error responses.
Published February 10, 2025
Without centralized exception handling, Spring returns a default error response that leaks internal details. @RestControllerAdvice lets you control exactly what error responses look like.
// Default Spring error — not what clients want to see
{
"timestamp": "2025-01-01T00:00:00.000+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/api/v1/orders/99"
}
@RestControllerAdvice
public class GlobalExceptionHandler {
// Custom error response record
record ErrorResponse(int status, String error, String message, Instant timestamp) {
static ErrorResponse of(int status, String error, String message) {
return new ErrorResponse(status, error, message, Instant.now());
}
}
@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(ResourceNotFoundException ex) {
return ErrorResponse.of(404, "Not Found", ex.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleValidation(MethodArgumentNotValidException ex) {
String message = ex.getBindingResult().getFieldErrors().stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.collect(Collectors.joining(", "));
return ErrorResponse.of(400, "Validation Failed", message);
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleGeneral(Exception ex, HttpServletRequest req) {
log.error("Unhandled exception at {}: {}", req.getRequestURI(), ex.getMessage(), ex);
return ErrorResponse.of(500, "Internal Server Error", "An unexpected error occurred");
}
}
public class AppException extends RuntimeException {
private final HttpStatus status;
public AppException(String message, HttpStatus status) {
super(message); this.status = status;
}
public HttpStatus getStatus() { return status; }
}
public class ResourceNotFoundException extends AppException {
public ResourceNotFoundException(String resource, String id) {
super(resource + " not found: " + id, HttpStatus.NOT_FOUND);
}
}
A common follow-up question: "How do you prevent sensitive information from leaking in error responses?"
Never expose stack traces, SQL queries, or internal class names in production. Log them server-side. Return only a safe, structured error message to the client. Use different error detail levels per environment (spring.profiles.active=prod suppresses details).