Skip to content
Spring Boot sb exceptions 3 min read

@ExceptionHandler

@ExceptionHandler marks a method that handles exceptions thrown by handler methods in the same controller. It is the most direct way to translate an exception into a specific HTTP status and body, keeping the mapping right next to the code that throws. This page covers method signatures, mapping to status codes, and handling several exception types at once.

A controller-local handler

Declare a method in your @RestController and annotate it with @ExceptionHandler, naming the exception type it handles. Any matching exception thrown by another method in that controller is routed to it.

@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {

    private final ProductService service;

    @GetMapping("/{id}")
    public Product one(@PathVariable Long id) {
        return service.findById(id)
                .orElseThrow(() -> new ProductNotFoundException(id));
    }

    @ExceptionHandler(ProductNotFoundException.class)
    public ResponseEntity<String> handleNotFound(ProductNotFoundException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
    }
}

Output:

HTTP/1.1 404 Not Found
Content-Type: text/plain;charset=UTF-8

Product 999 not found

The handler only fires for exceptions originating in ProductController. For app-wide behavior, move it to a @RestControllerAdvice.

Setting the status

There are two ways to attach a status to a handler’s response.

Option A — @ResponseStatus when the status is fixed and you return a plain body:

@ExceptionHandler(ProductNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ApiError handleNotFound(ProductNotFoundException ex) {
    return new ApiError(404, "PRODUCT_NOT_FOUND", ex.getMessage());
}

Option B — ResponseEntity when the status (or headers) varies:

@ExceptionHandler(OrderException.class)
public ResponseEntity<ApiError> handleOrder(OrderException ex) {
    HttpStatus status = ex.isRetryable() ? HttpStatus.SERVICE_UNAVAILABLE
                                         : HttpStatus.CONFLICT;
    return ResponseEntity.status(status)
            .body(new ApiError(status.value(), ex.getCode(), ex.getMessage()));
}

Note: If a handler returns a ResponseEntity, its status wins and any @ResponseStatus on the method is ignored. See ResponseEntity & Status Codes.

Handling multiple exception types

One handler can cover several related exceptions by listing them in the annotation. The parameter type must be a common supertype (often the exception base class or Exception).

@ExceptionHandler({ IllegalArgumentException.class, IllegalStateException.class })
public ResponseEntity<ApiError> handleBadRequest(RuntimeException ex) {
    var body = new ApiError(400, "BAD_REQUEST", ex.getMessage());
    return ResponseEntity.badRequest().body(body);
}

When several handlers could match, Spring picks the most specific one. A handler for ProductNotFoundException is preferred over a handler for RuntimeException.

If you declareIt catches
IllegalArgumentException.classexactly that type and its subtypes
{ A.class, B.class }either A or B (param = common supertype)
RuntimeException.classany unchecked exception not matched more specifically
Exception.classa catch-all fallback

Handler method signatures

@ExceptionHandler methods are flexible: Spring resolves their arguments by type, so you request only what you need. Supported parameters include the exception itself plus request context.

@ExceptionHandler(ValidationException.class)
public ResponseEntity<ApiError> handle(
        ValidationException ex,          // the thrown exception
        HttpServletRequest request,      // jakarta.servlet request
        WebRequest webRequest,           // Spring's request abstraction
        Locale locale) {                 // resolved locale

    var body = new ApiError(400, "VALIDATION", ex.getMessage());
    body.setPath(request.getRequestURI());
    return ResponseEntity.badRequest().body(body);
}

Common parameter and return types:

Parameter typeProvides
The exception typeThe thrown exception instance
HttpServletRequestRaw jakarta.servlet request (URI, headers)
WebRequestPortable request access
HttpHeadersRequest headers
Locale / TimeZoneResolved locale info
Return typeBehavior
ResponseEntity<T>Full control of status, headers, body
A DTO (+ @ResponseStatus)Serialized body with the declared status
ProblemDetailRFC 7807 body — see ProblemDetail
voidYou write the response yourself

Accessing the original request

Because you can inject HttpServletRequest, you can enrich the error body with the path or correlation headers — handy for tracing.

@ExceptionHandler(ProductNotFoundException.class)
public ResponseEntity<ApiError> handle(ProductNotFoundException ex,
                                        HttpServletRequest req) {
    var body = new ApiError(404, "PRODUCT_NOT_FOUND", ex.getMessage());
    body.setPath(req.getRequestURI());
    body.setTimestamp(Instant.now());
    return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body);
}

Output:

{
  "status": 404,
  "code": "PRODUCT_NOT_FOUND",
  "message": "Product 999 not found",
  "path": "/api/products/999",
  "timestamp": "2026-06-13T10:15:42.123Z"
}

Pitfalls

  • A controller-local handler does not see exceptions from other controllers — duplicate it or move it to a global advice.
  • Avoid catching Exception.class in a controller; reserve broad catch-alls for the global advice so you do not accidentally swallow framework exceptions.
  • Throwing a new exception from inside an @ExceptionHandler will propagate to the default error handling — handlers should not throw.
Last updated June 13, 2026
Was this helpful?