Start Learning
Javaneer
Back to stage
Module 2·Building Web APIs

Exception Handling & ProblemDetail

@ExceptionHandler, @ControllerAdvice, and standard RFC 9457 ProblemDetail error responses.

16 min readIntermediate
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:

Output
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.

A central complaints desk

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.

Two errors, one shape

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.
Was this lesson helpful?
Edit this page on GitHub