Every developer has written an API that returns 500 Internal Server Error with nothing else. It passes code review, the happy path works, and it ships. Then a customer's integration fails at 11pm, and somebody has to work out what went wrong from a status code and a timestamp.
I've been on both ends of that. As a full stack Java developer I shipped 150+ REST APIs in Spring Boot. As a Technical Support Engineer at Salesforce, I now spend my days on the receiving end of failing integrations. The single cheapest fix for most of that pain is also the most boring one: consistent, specific error responses.
What Spring Boot gives you by default
Out of the box, an unhandled exception in a Spring Boot REST controller returns a small JSON object with a timestamp, the status, a generic error name and the request path. The actual message is hidden by default (for good security reasons), and there is no stable way for a client to tell which kind of failure happened. A missing loan, an expired token and a database outage can all look almost identical to the caller.
Step 1: adopt ProblemDetail (RFC 9457)
Spring Framework 6 — the foundation of Spring Boot 3 and later — ships with first-class support for ProblemDetail, the standard error format defined in RFC 9457 (formerly RFC 7807). It gives every error the same shape: a type URI identifying the kind of error, a human-readable title, the HTTP status, a specific detail, and any extra properties you add.
For Spring MVC's own exceptions — bad JSON, unsupported media type, missing parameters — you can switch it on with a single property:
That property registers a handler only when you haven't written your own. In real projects you almost always will, so the next step matters more.
Step 2: one @RestControllerAdvice, one place for every error
Create domain-specific exceptions that carry the information a caller needs, then map them to ProblemDetail in a single @RestControllerAdvice. Extending ResponseEntityExceptionHandler means Spring's built-in exceptions are rendered as ProblemDetail too, so the property above becomes redundant.
Returning a ProblemDetail from an @ExceptionHandler is enough — Spring sets the status code and the application/problem+json content type for you.
Step 3: make validation errors point at the exact field
Validation failures are where generic errors hurt most. A form with twelve fields that returns "Bad Request" forces the user — or the support engineer — to guess. Override the validation handler and return every field error in one response:
The caller now receives something they can act on without opening a ticket:
Step 4: a safe catch-all for everything you didn't predict
Unexpected exceptions still need a response. The rule is simple: log everything, reveal nothing. The full stack trace goes to your logs, tagged with a correlation ID; the caller gets a polite message and that same ID to quote.
Where does correlationId come from? A small servlet filter that tags every request — I walk through it step by step in Correlation IDs and structured logging in Spring Boot.
The full stack half: using these errors in React
A consistent error shape pays off twice in a full stack application, because the frontend can handle every failure with one piece of code. Field errors go under their inputs; everything else becomes a banner that includes the reference ID.
type URI that clients can branch on, instead of parsing message strings. Keep the detail specific to this request. Return all validation errors at once, not one per round-trip. Always include a correlation ID. Keep server.error.include-stacktrace at its default of never. And document your error types in your OpenAPI spec — an error that's documented is an error that doesn't become a ticket.
Checklist before you ship your next endpoint
@RestControllerAdvice.