4.1 Global Exception Handling
← Back to Chapter 4: Exception Handling, Logging & Production Support
Never let unhandled raw stack traces leak to HTTP clients. Spring Boot provides @RestControllerAdvice to centralize exception interceptors across all controllers, returning consistent, machine-readable error responses.
What is Exception Handling?
An exception is an unexpected problem that occurs while the application is running.
For example:
User sends customer details
↓
Spring Boot API
↓
Database
↓
Exception occurs
↓
Application needs to handle it
Example:
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new RuntimeException("Customer not found"));
If the customer doesn't exist, an exception is thrown.
Without proper handling, the API may return something like:
500 Internal Server Error
with an unhelpful response.
We want something meaningful:
{
"status": 404,
"message": "Customer not found",
"timestamp": "2026-09-13T16:00:00"
}
Types of Exceptions in Java
At a high level:
Throwable
|
+-- Error
|
+-- Exception
|
+-- Checked Exception
|
+-- Runtime Exception
try-catch
The basic way to handle an exception is:
try {
// risky code
} catch (Exception e) {
// handling code
}
Example:
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("Cannot divide by zero");
}
But in Spring Boot REST applications, we normally don't put try-catch everywhere.
Why?
Because it makes code messy.
Instead, we can use global exception handling.
@ExceptionHandler
@ExceptionHandler is used to handle a particular exception inside a controller or exception handler class.
Example:
@RestController
public class CustomerController {
@GetMapping("/customer/{id}")
public Customer getCustomer(@PathVariable Long id) {
// Suppose exception occurs here
throw new CustomerNotFoundException("Customer not found");
}
@ExceptionHandler(CustomerNotFoundException.class)
public ResponseEntity<String> handleCustomerNotFound(
CustomerNotFoundException ex) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(ex.getMessage());
}
}
@ControllerAdvice
@ControllerAdvice provides centralized exception handling for controllers.
Example:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(CustomerNotFoundException.class)
public ResponseEntity<String> handleCustomerNotFound(
CustomerNotFoundException ex) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(ex.getMessage());
}
}
Now all controllers can use this handler.
@RestControllerAdvice
You will commonly see: @RestControllerAdvice instead of: @ControllerAdvice
The difference is:
@ControllerAdvice
↓
Used for centralized exception handling
@RestControllerAdvice
↓
@ControllerAdvice
+
@ResponseBody
For REST APIs, @RestControllerAdvice is usually convenient because responses are automatically written as JSON/body data.
1. RFC 7807 ProblemDetail in Spring Boot 3
Spring Boot 3 natively adopts the RFC 7807 Problem Details for HTTP APIs specification using the ProblemDetail class:
{
"type": "https://api.codewithsarvy.dev/errors/not-found",
"title": "Resource Not Found",
"status": 404,
"detail": "Customer with ID 4088 was not found",
"instance": "/api/v1/customers/4088",
"timestamp": "2026-09-11T14:30:00Z"
}
2. Implementing @RestControllerAdvice
Extend ResponseEntityExceptionHandler to inherit default handlers for standard Spring MVC exceptions (e.g., MethodArgumentNotValidException, HttpRequestMethodNotSupportedException):
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
// 1. Handle Custom Business Exceptions
@ExceptionHandler(ResourceNotFoundException.class)
public ProblemDetail handleResourceNotFound(ResourceNotFoundException ex, WebRequest request) {
log.warn("Resource not found: {}", ex.getMessage());
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
problem.setTitle("Resource Not Found");
problem.setType(URI.create("https://api.codewithsarvy.dev/errors/not-found"));
problem.setProperty("timestamp", Instant.now());
return problem;
}
// 2. Override Standard Validation Failures (Field Errors)
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request) {
Map<String, String> fieldErrors = new HashMap<>();
for (FieldError fe : ex.getBindingResult().getFieldErrors()) {
fieldErrors.put(fe.getField(), fe.getDefaultMessage());
}
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "Validation failed");
problem.setTitle("Invalid Request Payload");
problem.setProperty("invalid_fields", fieldErrors);
problem.setProperty("timestamp", Instant.now());
return ResponseEntity.badRequest().body(problem);
}
// 3. Fallback Catch-All for Unexpected Exceptions
@ExceptionHandler(Exception.class)
public ProblemDetail handleAllUncaught(Exception ex, WebRequest request) {
log.error("Unhandled internal server error occurred", ex);
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.INTERNAL_SERVER_ERROR,
"An unexpected internal error occurred. Please contact support."
);
problem.setTitle("Internal Server Error");
return problem;
}
}
3. Security Considerations
- Never Expose Database Errors: SQL syntax errors or database hostnames reveal internal architecture to attackers. Log the root cause internally, but return a sanitized client message.
- Trace IDs in Problem Details: Include a unique
correlationIdortraceIdso clients can quote it when contacting developer support.