Consistent error responses make APIs predictable for clients and keep controllers clean.
When a controller throws, Spring looks for an exception handler. Methods annotated @ExceptionHandler(SomeException.class) in a @RestControllerAdvice class apply to all controllers, so error handling lives in one place and controllers only deal with the happy path.
Spring supports the Problem Details standard (RFC 9457, formerly RFC 7807) through the ProblemDetail class, producing application/problem+json bodies with type, title, status, detail and instance, plus custom properties. Set spring.mvc.problemdetails.enabled=true to have Spring's own exceptions (400s for bad input, 405, 415 and so on) rendered this way too.
A typical setup:
- Define domain exceptions like
BookNotFoundException. - Map each to a status in the advice:
ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage()). - Extend
ResponseEntityExceptionHandlerto customise Spring's built-in exceptions, for example to list validation errors fromMethodArgumentNotValidException. - Add a catch-all
@ExceptionHandler(Exception.class)that logs the error and returns a generic 500 without leaking stack traces.
Alternatively throw ResponseStatusException(HttpStatus.NOT_FOUND, "...") directly for simple cases, or annotate an exception class with @ResponseStatus. Unhandled errors fall through to Boot's /error endpoint, which returns a basic JSON body.
Example
class BookNotFoundException extends RuntimeException {
BookNotFoundException(long id) { super("No book with id " + id); }
}
@RestControllerAdvice
class ApiErrors {
private static final Logger log = LoggerFactory.getLogger(ApiErrors.class);
@ExceptionHandler(BookNotFoundException.class)
ProblemDetail notFound(BookNotFoundException ex) {
ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
pd.setTitle("Book not found");
pd.setProperty("code", "BOOK_NOT_FOUND");
return pd;
}
@ExceptionHandler(Exception.class)
ProblemDetail unexpected(Exception ex) {
log.error("Unhandled error", ex);
return ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, "Unexpected error");
}
}
// GET /api/books/42 ->
// 404 {"type":"about:blank","title":"Book not found","status":404,
// "detail":"No book with id 42","instance":"/api/books/42","code":"BOOK_NOT_FOUND"}Remember
@RestControllerAdvice+@ExceptionHandlercentralise error mapping- Return
ProblemDetailfor standard application/problem+json bodies - Map domain exceptions to statuses; never leak stack traces
ResponseStatusExceptioncovers simple one-off cases
Watch out for
- Catching exceptions in every controller method and building error responses by hand, inconsistently.