Exception Handling Intro
When something goes wrong in a REST API — a missing record, invalid input, a downstream timeout — the response your clients receive is part of your public contract. Letting exceptions leak as raw stack traces or inconsistent payloads makes APIs hard to consume. Centralized exception handling turns exceptions into clean, predictable HTTP responses you design once and reuse everywhere.
Why centralized handling
Without a strategy, every controller ends up wrapping calls in try/catch and building ad-hoc ResponseEntity objects. The result is duplicated code, inconsistent status codes, and error bodies that differ from endpoint to endpoint. Spring Boot lets you push all of that to a single place so controllers stay focused on the happy path and throw plain exceptions.
// Controller stays clean — no try/catch, no status plumbing
@GetMapping("/{id}")
public Product one(@PathVariable Long id) {
return service.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Product", id));
}
A global handler then maps ResourceNotFoundException to 404 with a structured body — for every controller at once.
The default Spring Boot error response
Spring Boot ships an ErrorController (BasicErrorController) that catches anything that escapes your handlers. For browser requests it renders the famous Whitelabel Error Page; for API clients (those sending Accept: application/json) it returns a JSON body assembled by DefaultErrorAttributes.
curl -s http://localhost:8080/api/products/999 -H "Accept: application/json"
Output:
{
"timestamp": "2026-06-13T10:15:42.123+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/api/products/999"
}
This default is a useful safety net, but it is generic: there is no machine-readable error code, no field-level detail, and (by design) no message or stack trace in production. You will almost always replace it with your own structured responses.
Controlling the default with server.error.*
You can tune what the default error attributes expose without writing code. These keys are read by ErrorProperties:
# Include the exception message in the body (NEVER, ALWAYS, ON_PARAM)
server.error.include-message=on_param
# Include binding/validation errors
server.error.include-binding-errors=on_param
# Include the stack trace (avoid ALWAYS in production)
server.error.include-stacktrace=never
# Include the exception class name
server.error.include-exception=false
# Path the dispatcher forwards errors to
server.error.path=/error
# Turn the Whitelabel HTML page off
server.error.whitelabel.enabled=false
| Property | Default | Notes |
|---|---|---|
server.error.include-message | never | Hidden in prod to avoid leaking internals |
server.error.include-stacktrace | never | always is a security risk |
server.error.include-binding-errors | never | Set on_param to debug with ?message=... |
server.error.whitelabel.enabled | true | Set false to disable the HTML page |
Warning: Never set
include-stacktrace=alwaysin production. Stack traces reveal class names, library versions, and code paths that help attackers.
The two layers of handling
Spring offers handling at two scopes. You typically use both.
| Layer | Annotation | Scope | Use for |
|---|---|---|---|
| Per-controller | @ExceptionHandler (on a controller) | One controller class | Exceptions unique to that controller |
| Global | @ExceptionHandler inside @RestControllerAdvice | All controllers | App-wide policy, the common case |
A per-controller method handles only exceptions thrown by that controller. A @RestControllerAdvice bean handles exceptions across every controller, which is where most teams centralize their policy.
@RestControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
ApiError notFound(ResourceNotFoundException ex) {
return new ApiError(404, "NOT_FOUND", ex.getMessage());
}
}
Tip: Start with one
@RestControllerAdvicefor the whole application and only drop to per-controller handlers when an exception genuinely needs controller-specific treatment.
How a request flows to a handler
- A controller method throws (or a filter/argument resolver does).
- Spring MVC’s
HandlerExceptionResolverchain looks for a matching@ExceptionHandler— first on the controller, then in any@ControllerAdvice. - If one matches, its return value becomes the response (serialized to JSON).
- If none matches, the request is forwarded to
/errorandBasicErrorControllerproduces the default response above.
This means your handlers always win over the default — you are layering custom behavior on top of a guaranteed fallback.
In This Section
- @ExceptionHandler — controller-local handler methods and their signatures.
- @RestControllerAdvice — global handling, ordering, and scoping.
- Custom Exceptions — designing domain exceptions that carry context.
- Structured Error Responses — a consistent error DTO across the API.
- ProblemDetail (RFC 7807) — the standardized
application/problem+jsonformat.