The problem with inconsistent error responses
You've probably seen it before: one endpoint returns {"error": "Not found"}, another returns {"message": "User not found", "code": 404}, and a third just dumps a stack trace. Frontend teams waste time parsing different shapes. Mobile apps crash on unexpected fields. Debugging becomes a nightmare. Global error handling with @RestControllerAdvice explained is how we solve this—by returning a single, predictable error format from every endpoint in your Spring Boot application.
In this article, we'll walk through setting up RFC 9457 problem details, mapping your domain exceptions to HTTP responses, protecting sensitive information, and handling validation errors in a way that lets frontend teams highlight form fields without guessing.
Enable problem details — Spring Boot can return RFC 9457 responses out of the box
Spring Boot 2.9 introduced native support for RFC 9457 problem details. This is a standardized JSON format that the web industry has agreed on. Instead of rolling your own error shape, you get a consistent structure that clients can rely on.
To enable it, add one line to your application.yml:
spring:
mvc:
problemdetails:
enabled: trueThat's it. Now when Spring catches validation errors or throws exceptions it recognizes, it will return responses like this:
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"detail": "Failed to read request",
"instance": "/api/users"
}The fields follow the RFC 9457 spec: type (a URI identifying the error class), title (human-readable summary), status (HTTP status code), detail (what went wrong), and instance (which endpoint threw it). Clients can parse this consistently across all your services.
However, Spring's built-in problem details only handles framework-level errors. Your business logic exceptions won't be caught automatically. That's where @RestControllerAdvice comes in.
Map domain exceptions — Translate your exceptions to status codes in one place
When a user tries to fetch a resource that doesn't exist, you throw NotFoundException. When they violate a business rule, you throw BusinessRuleViolation. These are your domain exceptions, and they need to map to HTTP status codes and problem detail responses.
Instead of handling errors in every controller method, you centralize the logic in a single @RestControllerAdvice class:
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalErrorHandler {
@ExceptionHandler(NotFoundException.class)
public ProblemDetail notFound(NotFoundException e) {
return ProblemDetail.forStatusAndDetail(
HttpStatus.NOT_FOUND,
e.getMessage()
);
}
@ExceptionHandler(BusinessRuleViolation.class)
public ProblemDetail businessRuleViolation(BusinessRuleViolation e) {
return ProblemDetail.forStatusAndDetail(
HttpStatus.UNPROCESSABLE_ENTITY,
e.getMessage()
);
}
@ExceptionHandler(UnauthorizedException.class)
public ProblemDetail unauthorized(UnauthorizedException e) {
return ProblemDetail.forStatusAndDetail(
HttpStatus.UNAUTHORIZED,
"Authentication required"
);
}
}Now whenever NotFoundException is thrown from any controller or service, Spring catches it, runs the handler, and returns a 404 problem detail response. Your controller methods stay clean—they just throw exceptions. No try-catch blocks scattered everywhere.
You can customize the problem detail further if needed:
@ExceptionHandler(NotFoundException.class)
public ProblemDetail notFound(NotFoundException e) {
ProblemDetail pd = ProblemDetail.forStatusAndDetail(
HttpStatus.NOT_FOUND,
e.getMessage()
);
pd.setType(URI.create("https://api.example.com/errors/not-found"));
pd.setTitle("Resource Not Found");
pd.setProperty("resource_id", e.getResourceId());
return pd;
}The setProperty method lets you add custom fields to the JSON response. This is useful for embedding context that your frontend needs.
Never leak stack traces — Log details server-side; return a safe message and a trace id
One of the worst security mistakes is returning full stack traces to clients. They reveal your framework versions, internal class names, database structure, and more—all valuable information for attackers. Yet you still need to debug production issues.
The solution: log everything server-side with a trace ID, and return only that ID to the client. Frontend teams can report the trace ID to your support team, who can look it up in your logs.
Here's a real implementation:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import java.util.UUID;
@RestControllerAdvice
public class GlobalErrorHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalErrorHandler.class);
@ExceptionHandler(Exception.class)
public ProblemDetail handleGenericException(Exception e) {
String traceId = UUID.randomUUID().toString();
MDC.put("trace_id", traceId);
log.error("Unhandled exception", e);
ProblemDetail pd = ProblemDetail.forStatusAndDetail(
HttpStatus.INTERNAL_SERVER_ERROR,
"An error occurred. Please contact support with trace ID: " + traceId
);
pd.setProperty("trace_id", traceId);
MDC.remove("trace_id");
return pd;
}
}The MDC (Mapped Diagnostic Context) ensures that the trace ID appears in all log lines during request processing. Your logging framework (like Logback) can be configured to include it:
%d{yyyy-MM-dd HH:mm:ss} [%X{trace_id}] %-5level %logger{36} - %msg%nWhen a user reports "I got trace ID abc123", you grep your logs for that ID and see exactly what happened.
Validation errors — Return field-level errors so frontends can highlight inputs
Form validation is common, and frontend teams need to know which fields are invalid so they can highlight them with error messages. Spring's @Valid annotation and MethodArgumentNotValidException are the standard way to handle this.
By default, Spring returns a generic problem detail for validation errors. You can customize it to include field-level details:
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.validation.FieldError;
@RestControllerAdvice
public class GlobalErrorHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ProblemDetail validationError(MethodArgumentNotValidException e) {
Map<String, String> fieldErrors = new HashMap<>();
e.getBindingResult().getFieldErrors().forEach(error ->
fieldErrors.put(error.getField(), error.getDefaultMessage())
);
ProblemDetail pd = ProblemDetail.forStatusAndDetail(
HttpStatus.BAD_REQUEST,
"Validation failed"
);
pd.setProperty("fields", fieldErrors);
return pd;
}
}This returns a response like:
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"detail": "Validation failed",
"instance": "/api/users",
"fields": {
"email": "Email should be valid",
"name": "Name must not be blank"
}
}Frontend code can loop through the fields object and attach error messages to the corresponding input elements. This gives users a smooth, professional experience without writing error-handling code in your controllers.
Common mistakes
Swallowing exceptions silently. Don't catch exceptions in your handler and return a generic "something went wrong" response every time. Log them. You need that information for debugging production issues.
Returning different status codes for the same error. If a user is not found, always return 404. Don't sometimes return 400. Consistency helps clients write robust error handling.
Forgetting that @RestControllerAdvice only catches exceptions. It won't handle errors in filters or security configurations. For those, you need separate configuration. Test your error handling end-to-end.
Including sensitive data in error messages. If a resource belongs to another user and they try to access it, don't say "User 42 cannot access resource owned by user 7." Just say "Not found." Attackers use these details to enumerate your data.
Not handling validation errors separately. Generic exceptions and validation errors need different handling. Treat them distinctly so frontends can respond appropriately.
Key takeaways
- Enable
spring.mvc.problemdetails.enabled: truein your config to get RFC 9457 responses with a standard structure. - Use
@RestControllerAdviceto centralize exception handling and map your domain exceptions to HTTP status codes. - Never return stack traces to clients. Log details server-side with trace IDs, and return only the trace ID to the client for support reference.
- Handle validation errors separately and return field-level error details so frontends can highlight form inputs.
- Keep error responses consistent across all endpoints. Clients rely on a predictable error shape to build reliable integrations.






Comments
Loading comments…