Exception Handling & ProblemDetail
@ExceptionHandler, @ControllerAdvice, and standard RFC 9457 ProblemDetail error responses.
On this page
When something goes wrong - a book isn't found, validation fails, a duplicate is created - your API must respond with the right status code and a useful, consistent error body. Scattering try/catch through every controller is the wrong way. Spring gives you a clean, centralized approach built around a web standard.
ProblemDetail: a standard error shape
Ad-hoc error JSON ({"error": "not found"} here, {"message": "..."} there) forces
every client to guess. ProblemDetail (RFC 9457) is a standard structure Spring
supports out of the box:
HTTP/1.1 404 Not Found
Content-Type: application/problem+json
{
"type": "https://bookvault.dev/errors/book-not-found",
"title": "Book not found",
"status": 404,
"detail": "No book found with ISBN 000-0000000000",
"instance": "/api/books/000-0000000000"
}Every error in your API can share this shape, so clients parse failures the same way every time.
Centralize with @RestControllerAdvice
A class annotated @RestControllerAdvice holds @ExceptionHandler methods that apply
across all controllers. One place maps each exception to a response:
@RestControllerAdvice
class ApiExceptionHandler {
@ExceptionHandler(BookNotFoundException.class)
ProblemDetail handleNotFound(BookNotFoundException ex) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.NOT_FOUND, ex.getMessage());
problem.setTitle("Book not found");
return problem;
}
@ExceptionHandler(MethodArgumentNotValidException.class)
ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
problem.setTitle("Validation failed");
problem.setProperty("errors", ex.getBindingResult().getFieldErrors().stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage()).toList());
return problem;
}
}Your controllers now stay clean - they just throw meaningful exceptions
(throw new BookNotFoundException(isbn)), and the advice turns each into a proper
response.
Imagine every department in a company handling customer complaints its own way -
inconsistent, chaotic. A central complaints desk takes every issue, categorizes
it, and responds in one consistent format. @RestControllerAdvice is that desk: all
exceptions funnel to it, and it produces uniform, well-formed error responses.
Extend the ready-made base class
For common Spring MVC exceptions, extend ResponseEntityExceptionHandler - it already
maps things like unreadable bodies and validation errors to ProblemDetail. Override
only what you want to customize, and add handlers for your own exceptions.
Don't leak internals
A final rule: never return raw stack traces or exception messages from unexpected
errors. Map unknown exceptions to a generic 500 ProblemDetail (log the details
server-side), so you never expose internals to clients.
BookVault must return 404 when a book isn't found and 400 when a create request
fails validation - both as ProblemDetail bodies. Where do you put the mapping so no
controller needs a try/catch, and what does each controller method do to trigger it?
What is the purpose of ProblemDetail (RFC 9457) in a Spring API?
Key takeaways
- Handle errors centrally with a @RestControllerAdvice class of @ExceptionHandler methods - not try/catch in every controller.
- Return ProblemDetail (RFC 9457): a standard type/title/status/detail structure, so clients parse all errors the same way.
- Controllers stay clean and just throw meaningful exceptions (e.g. BookNotFoundException); the advice maps each to a response.
- Extend ResponseEntityExceptionHandler to reuse Spring's built-in ProblemDetail mappings.
- Never leak stack traces or internal messages - map unexpected errors to a generic 500 and log details server-side.