Skip to content
Spring Boot sb exceptions 3 min read

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
PropertyDefaultNotes
server.error.include-messageneverHidden in prod to avoid leaking internals
server.error.include-stacktraceneveralways is a security risk
server.error.include-binding-errorsneverSet on_param to debug with ?message=...
server.error.whitelabel.enabledtrueSet false to disable the HTML page

Warning: Never set include-stacktrace=always in 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.

LayerAnnotationScopeUse for
Per-controller@ExceptionHandler (on a controller)One controller classExceptions unique to that controller
Global@ExceptionHandler inside @RestControllerAdviceAll controllersApp-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 @RestControllerAdvice for 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

  1. A controller method throws (or a filter/argument resolver does).
  2. Spring MVC’s HandlerExceptionResolver chain looks for a matching @ExceptionHandler — first on the controller, then in any @ControllerAdvice.
  3. If one matches, its return value becomes the response (serialized to JSON).
  4. If none matches, the request is forwarded to /error and BasicErrorController produces 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

Last updated June 13, 2026
Was this helpful?