Skip to main content

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 correlationId or traceId so clients can quote it when contacting developer support.