Error handling and ProblemDetail

Spring Boot · Practical · 14 min

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 ResponseEntityExceptionHandler to customise Spring's built-in exceptions, for example to list validation errors from MethodArgumentNotValidException.
  • 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 + @ExceptionHandler centralise error mapping
  • Return ProblemDetail for standard application/problem+json bodies
  • Map domain exceptions to statuses; never leak stack traces
  • ResponseStatusException covers simple one-off cases

Watch out for

  • Catching exceptions in every controller method and building error responses by hand, inconsistently.

Check yourself

  1. Where should a handler live so it applies to exceptions from all controllers?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.spring.io/spring-framework/reference/web/webmvc/mvc-ann-rest-exceptions.html

Builds on

Unlocks

Nothing builds on this yet.