How do you handle exceptions globally in a Spring REST API (@ControllerAdvice/@ExceptionHandler)?

8 minintermediateexceptionhandlercontrolleradviceerror-handling

Quick Answer

@ExceptionHandler on a method inside a controller catches exceptions thrown by that controller's own handler methods, letting you return a specific, structured error response instead of letting the exception propagate to a generic error page. @ControllerAdvice (or @RestControllerAdvice for REST APIs) makes those @ExceptionHandler methods apply globally, across every controller in the application, centralizing consistent error-response formatting instead of duplicating try/catch logic in every endpoint.

Detailed Answer

Rather than wrapping every controller method in a try/catch, Spring MVC lets you declare exception-handling logic separately, and have it apply automatically wherever a matching exception is thrown.

@ExceptionHandler on a method inside a single controller — handles exceptions thrown by that controller's handler methods only:

@RestController
class OrderController {
    @GetMapping("/orders/{id}")
    Order getOrder(@PathVariable Long id) {
        return repository.findById(id).orElseThrow(() -> new OrderNotFoundException(id));
    }

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

@RestControllerAdvice (or @ControllerAdvice + @ResponseBody) makes @ExceptionHandler methods apply globally, across every controller in the application — the standard way to centralize consistent error formatting instead of duplicating handling logic per-controller:

@RestControllerAdvice
class GlobalExceptionHandler {
    @ExceptionHandler(OrderNotFoundException.class)
    ResponseEntity<ErrorResponse> handleNotFound(OrderNotFoundException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
            .body(new ErrorResponse("ORDER_NOT_FOUND", ex.getMessage()));
    }

    @ExceptionHandler(Exception.class) // catch-all fallback
    ResponseEntity<ErrorResponse> handleGeneric(Exception ex) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(new ErrorResponse("INTERNAL_ERROR", "An unexpected error occurred"));
    }
}

Resolution order: Spring matches the most specific applicable @ExceptionHandler — a handler for the exact thrown exception type wins over a handler for one of its superclasses, and a handler defined on the specific controller itself (via a local @ExceptionHandler) takes precedence over a matching global @ControllerAdvice handler.

Practical benefits of centralizing this in a @RestControllerAdvice:

  • Consistent error response shape across the entire API (a code/message/timestamp structure, for example), which API consumers can rely on uniformly.
  • Avoids leaking internal exception details/stack traces to clients — the handler controls exactly what's exposed.
  • Keeps controller methods focused on their actual logic, free of repetitive try/catch boilerplate.

Spring Boot also ships a default fallback (ErrorController/BasicErrorController) that produces a generic JSON error body for any exception not otherwise handled — a @RestControllerAdvice typically exists specifically to produce something more specific and informative than that generic default.