REST Exception Handling
Definition
REST exception handling means turning exceptions into proper HTTP responses in one place, using @RestControllerAdvice and @ExceptionHandler. Spring 6 supports ProblemDetail, a standard format for error bodies.
Why it exists
Handling errors inside every controller repeats code and gives inconsistent responses. One central handler defines the mapping and format once.
How it works
The layers
- Boot's default
BasicErrorControllerand/error: returns a generic JSON body for uncaught exceptions. Hide details withserver.error.include-message=neverandinclude-stacktrace=never(these are the defaults in recent versions). @ResponseStatuson an exception class: maps it to a status code.ResponseStatusException: throw inline with a status and reason.@ExceptionHandlerin a controller: handles exceptions for that controller only.@RestControllerAdvice: global handlers across all controllers. This is the standard solution.
Standard error body: ProblemDetail
Spring Framework 6 supports RFC 9457 "Problem Details for HTTP APIs" with the ProblemDetail class. Enable it for Spring's own exceptions with spring.mvc.problemdetails.enabled=true, or extend ResponseEntityExceptionHandler which handles them.
How resolution works
DispatcherServlet asks its HandlerExceptionResolver chain: ExceptionHandlerExceptionResolver (your @ExceptionHandler methods), then ResponseStatusExceptionResolver, then DefaultHandlerExceptionResolver. The most specific handler for the exception type wins; local controller handlers take precedence over advice.
Exceptions thrown from filters happen before the DispatcherServlet, so @ControllerAdvice does not see them. Handle those in the filter or with a HandlerExceptionResolver.
Example code
Example
Domain exceptions:
public class OrderNotFoundException extends RuntimeException {
private final long orderId;
public OrderNotFoundException(long orderId) {
super("Order " + orderId + " not found");
this.orderId = orderId;
}
public long getOrderId() { return orderId; }
}
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(OrderNotFoundException.class)
ProblemDetail handleNotFound(OrderNotFoundException ex) {
ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
pd.setTitle("Order not found");
pd.setProperty("orderId", ex.getOrderId());
return pd;
}
@ExceptionHandler(DataIntegrityViolationException.class)
ProblemDetail handleConflict(DataIntegrityViolationException ex) {
return ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, "The request conflicts with existing data");
}
@ExceptionHandler(Exception.class)
ProblemDetail handleUnexpected(Exception ex) {
log.error("Unhandled exception", ex);
return ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, "An unexpected error occurred");
}
}
Response for a missing order:
{
"type": "about:blank",
"title": "Order not found",
"status": 404,
"detail": "Order 42 not found",
"instance": "/orders/42",
"orderId": 42
}
Validation errors (MethodArgumentNotValidException) are covered in Validation.
System design considerations
- Include a correlation id in error responses and logs so support can trace requests
- Distinguish client errors (4xx) from server errors (5xx) in alerting
- Version error codes carefully, since clients depend on them
When to use it, and when not
Use it when
- Several endpoints share error handling
- You want a standard error body clients can rely on
- You translate domain exceptions to HTTP statuses
Think twice when
- You return internal messages or stack traces to clients
- You catch exceptions in controllers and swallow context
- You use 200 with an error flag instead of correct statuses
Common mistakes
- Catching
Exceptionin the controller and returning 200 with an error message. - Generic handler registered before specific ones with unclear precedence; rely on type specificity and test it.
- Logging the same exception at every layer, producing duplicate stack traces.
- Forgetting that filter exceptions bypass the advice.
Interview questions
1. How do you handle exceptions globally?
A class annotated with @RestControllerAdvice containing @ExceptionHandler methods; each maps an exception type to a response.
2. @ControllerAdvice vs @RestControllerAdvice?
The latter adds @ResponseBody, so return values are serialised to the body instead of being interpreted as view names.
3. Which HTTP status for which error?
400 for malformed or invalid input, 401 unauthenticated, 403 forbidden, 404 not found, 409 conflict (duplicate or version clash), 422 semantic validation failure (optional alternative to 400), 429 rate limited, 500 server bug, 503 dependency unavailable.
4. How do you avoid leaking internals?
Return generic messages for unexpected errors, log the details with a correlation ID, and never expose stack traces or SQL errors.
5. A transactional method throws a checked exception. Does it roll back?
By default no: only RuntimeException and Error trigger rollback. Use @Transactional(rollbackFor = ...). See Transactions.
Check yourself
1Where should REST error mapping live?
2What is ProblemDetail?
3Two handlers match an exception. Which wins?
Key takeaways
- Use one
@RestControllerAdvicewith typed handlers and a consistentProblemDetailbody. - Map domain exceptions to precise status codes; log unexpected ones once.
- Filters and security failures need their own handling.
Next: Validation.
Next topicTesting: JUnit and MockitoFinished this page? Track your progress (saved only in your browser).