Spring Boot Exception Handling
1. Introduction to Exception Handling in Spring Boot
Spring Boot provides robust exception handling mechanisms. These mechanisms ensure that the application gracefully handles exceptions and returns meaningful error messages to the client. Below are the main ways to handle exceptions in Spring Boot:
- Using @ExceptionHandler
- Using @ControllerAdvice
- Using ResponseStatusException
- Custom error responses
2. @ExceptionHandler Annotation
The @ExceptionHandler annotation is used within controller classes to handle specific exceptions that occur during the execution of a request.
Example: Handling a specific exception like NullPointerException.
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ExceptionController {
@GetMapping("/test")
public String test() {
throw new NullPointerException("Test Null Pointer Exception");
}
@ExceptionHandler(NullPointerException.class)
public String handleNullPointerException(NullPointerException ex) {
return "Caught Null Pointer Exception: " + ex.getMessage();
}
}
3. Global Exception Handling using @ControllerAdvice
The @ControllerAdvice annotation is used to define a global exception handler that applies to all controllers. This is a centralized way to manage exceptions.
Example: Creating a global exception handler for all exceptions.
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
@ResponseBody
public ResponseEntity handleAllExceptions(Exception ex) {
return new ResponseEntity<>("Global Exception Handler: " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
4. Custom Exception Handling with @ResponseStatus
The @ResponseStatus annotation allows you to map exceptions to specific HTTP status codes. This provides more control over the response status returned when an exception occurs.
Example: Custom exception with specific HTTP status.
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
Controller using the custom exception:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MyController {
@GetMapping("/resource")
public String getResource() {
throw new ResourceNotFoundException("Resource not found");
}
}
5. Exception Handling with ResponseStatusException
The ResponseStatusException class can be used to throw exceptions with a specific HTTP status and message directly from the controller.
Example: Throwing an exception using ResponseStatusException.
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.http.HttpStatus;
@RestController
public class MyController {
@GetMapping("/error")
public String error() {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid Request");
}
}
6. Custom Error Response Structure
We can create a custom error response structure by defining a custom error class and using it in exception handlers.
Step 1: Define a custom error response class.
public class CustomErrorResponse {
private String errorMessage;
private String errorCode;
private String timestamp;
public CustomErrorResponse(String errorMessage, String errorCode, String timestamp) {
this.errorMessage = errorMessage;
this.errorCode = errorCode;
this.timestamp = LocalDateTime.now();;
}
// Getters and setters
}
Step 2: Use the custom error response in the global exception handler.
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.http.ResponseEntity;
import java.time.LocalDateTime;
@ControllerAdvice
public class CustomGlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity handleGlobalException(Exception ex) {
CustomErrorResponse errorResponse = new CustomErrorResponse(ex.getMessage(), "ERROR-001");
return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler(ProductAlreadyExistsException.class)
public ResponseEntity handleProductAlreadyExistsException(ProductAlreadyExistsException ex) {
CustomErrorResponse errorResponse = new CustomErrorResponse(ex.getMessage(), "PRODUCT_EXISTS");
return new ResponseEntity<>(errorResponse, HttpStatus.CONFLICT);
}
}
7. Handling Validation Exceptions
Spring Boot automatically handles validation using annotations like @Valid and @Validated, but custom validation exceptions can also be handled.
Example: Handling validation exceptions using @ControllerAdvice.
// DTO Class with Validation
public class ProductRequest {
@NotNull(message = "Name is required")
private String name;
@Min(value = 1, message = "Price must be greater than zero")
private double price;
// Getters and Setters
}
// Controller
@RestController
public class ProductController {
@PostMapping("/product")
public ResponseEntity createProduct(@Valid @RequestBody ProductRequest productRequest) {
// Process the product request
return new ResponseEntity<>(new Product(1, productRequest.getName()), HttpStatus.CREATED);
}
}
// Global Exception Handler
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity
- The @Valid annotation in ProductController triggers validation for the incoming request.
- In case of validation failure, MethodArgumentNotValidException is thrown and handled by the GlobalExceptionHandler.
- The handler extracts validation messages and returns them in a structured format (e.g., a map of field names and errors).
8. Sample JSON Error Response
Below is an example of how a typical custom error response might look when handling an exception:
{
"errorMessage": "Resource not found",
"errorCode": "ERROR-404",
"timestamp": "2024-09-30T15:23:01"
}
9. Summary
- @ExceptionHandler is used for local exception handling in a controller.
- @ControllerAdvice is used for global exception handling across all controllers.
- @ResponseStatus maps an exception to a specific HTTP status code.
- ResponseStatusException is used to throw exceptions directly from the controller with an HTTP status.
- Custom error responses can be structured using a custom error class.
Interview Questions and Answers
Q1: You have a Spring Boot application where you are handling exceptions globally using @ControllerAdvice.
However, in one specific controller method, you want to handle exceptions differently and avoid global handling.
How would you achieve this?
You can exclude specific methods from being handled by @ControllerAdvice by using @ExceptionHandler directly within the controller method itself.
This allows for a more fine-grained exception handling strategy.
// Global exception handler using @ControllerAdvice
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity handleResourceNotFound(ResourceNotFoundException ex) {
return new ResponseEntity<>("Global Handler: " + ex.getMessage(), HttpStatus.NOT_FOUND);
}
}
// Controller with method-specific exception handling
@RestController
@RequestMapping("/api")
public class MyController {
// This method will use the global exception handler
@GetMapping("/resource/{id}")
public ResponseEntity getResource(@PathVariable("id") String id) {
if ("invalid".equals(id)) {
throw new ResourceNotFoundException("Resource not found!");
}
return new ResponseEntity<>("Resource found: " + id, HttpStatus.OK);
}
// Custom exception handling specific to this method
@GetMapping("/special/{id}")
public ResponseEntity getSpecialResource(@PathVariable("id") String id) {
if ("invalid".equals(id)) {
throw new SpecialResourceNotFoundException("Special resource not found!");
}
return new ResponseEntity<>("Special resource found: " + id, HttpStatus.OK);
}
// Method-specific exception handler
@ExceptionHandler(SpecialResourceNotFoundException.class)
public ResponseEntity handleSpecialResourceNotFound(SpecialResourceNotFoundException ex) {
return new ResponseEntity<>("Special Handler: " + ex.getMessage(), HttpStatus.NOT_FOUND);
}
}
- The first endpoint /resource/{id} will use the global exception handler.
- The second endpoint /special/{id} will handle exceptions using the method-specific @ExceptionHandler.
2. Your application interacts with multiple microservices, and you want to customize the exception handling for HttpClientErrorException
and HttpServerErrorException (from RestTemplate) differently. How would you achieve this in Spring Boot?
You can create custom exception handling for specific exceptions related to HTTP client and server errors by using @ControllerAdvice with multiple @ExceptionHandler methods.
// Exception handler for RestTemplate HTTP exceptions
@ControllerAdvice
public class RestTemplateExceptionHandler {
@ExceptionHandler(HttpClientErrorException.class)
public ResponseEntity handleClientError(HttpClientErrorException ex) {
return new ResponseEntity<>("Client error: " + ex.getStatusCode() + " - " + ex.getResponseBodyAsString(), HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(HttpServerErrorException.class)
public ResponseEntity handleServerError(HttpServerErrorException ex) {
return new ResponseEntity<>("Server error: " + ex.getStatusCode() + " - " + ex.getResponseBodyAsString(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
// Service that uses RestTemplate to make HTTP calls
@Service
public class ExternalService {
@Autowired
private RestTemplate restTemplate;
public String callExternalService(String url) {
try {
return restTemplate.getForObject(url, String.class);
} catch (HttpClientErrorException | HttpServerErrorException ex) {
throw ex; // Propagate exception for custom handling
}
}
}
// Controller using the external service
@RestController
@RequestMapping("/external")
public class ExternalServiceController {
@Autowired
private ExternalService externalService;
@GetMapping("/data")
public ResponseEntity getExternalData() {
String url = "https://some-external-api.com/resource";
return new ResponseEntity<>(externalService.callExternalService(url), HttpStatus.OK);
}
}
Q3: You have multiple layers in your Spring Boot application (controller, service, repository),
and exceptions thrown in the repository layer should be converted into custom service-layer exceptions.
How would you propagate exceptions from one layer to another while maintaining clean separation of concerns?
You should catch low-level exceptions (e.g., DataAccessException from Spring Data) in the service layer and
rethrow them as service-specific exceptions. This allows you to hide implementation details from the higher layers.
// Custom service-layer exception
public class ServiceException extends RuntimeException {
public ServiceException(String message, Throwable cause) {
super(message, cause);
}
}
// Service layer that catches repository exceptions
@Service
public class MyService {
@Autowired
private MyRepository myRepository;
public MyEntity getEntityById(Long id) {
try {
return myRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Entity not found"));
} catch (DataAccessException ex) {
// Wrap the low-level exception into a service-level exception
throw new ServiceException("Failed to retrieve entity from database", ex);
}
}
}
// Repository layer (Spring Data JPA)
@Repository
public interface MyRepository extends JpaRepository {}
// Controller layer that calls the service
@RestController
@RequestMapping("/entity")
public class MyController {
@Autowired
private MyService myService;
@GetMapping("/{id}")
public ResponseEntity getEntity(@PathVariable Long id) {
return new ResponseEntity<>(myService.getEntityById(id), HttpStatus.OK);
}
// Custom exception handler for service exceptions
@ExceptionHandler(ServiceException.class)
public ResponseEntity handleServiceException(ServiceException ex) {
return new ResponseEntity<>("Service error: " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
You can exclude specific methods from being handled by @ControllerAdvice by using @ExceptionHandler directly within the controller method itself. This allows for a more fine-grained exception handling strategy.
// Global exception handler using @ControllerAdvice
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity handleResourceNotFound(ResourceNotFoundException ex) {
return new ResponseEntity<>("Global Handler: " + ex.getMessage(), HttpStatus.NOT_FOUND);
}
}
// Controller with method-specific exception handling
@RestController
@RequestMapping("/api")
public class MyController {
// This method will use the global exception handler
@GetMapping("/resource/{id}")
public ResponseEntity getResource(@PathVariable("id") String id) {
if ("invalid".equals(id)) {
throw new ResourceNotFoundException("Resource not found!");
}
return new ResponseEntity<>("Resource found: " + id, HttpStatus.OK);
}
// Custom exception handling specific to this method
@GetMapping("/special/{id}")
public ResponseEntity getSpecialResource(@PathVariable("id") String id) {
if ("invalid".equals(id)) {
throw new SpecialResourceNotFoundException("Special resource not found!");
}
return new ResponseEntity<>("Special resource found: " + id, HttpStatus.OK);
}
// Method-specific exception handler
@ExceptionHandler(SpecialResourceNotFoundException.class)
public ResponseEntity handleSpecialResourceNotFound(SpecialResourceNotFoundException ex) {
return new ResponseEntity<>("Special Handler: " + ex.getMessage(), HttpStatus.NOT_FOUND);
}
}
- The first endpoint /resource/{id} will use the global exception handler.
- The second endpoint /special/{id} will handle exceptions using the method-specific @ExceptionHandler.
You can create custom exception handling for specific exceptions related to HTTP client and server errors by using @ControllerAdvice with multiple @ExceptionHandler methods.
// Exception handler for RestTemplate HTTP exceptions
@ControllerAdvice
public class RestTemplateExceptionHandler {
@ExceptionHandler(HttpClientErrorException.class)
public ResponseEntity handleClientError(HttpClientErrorException ex) {
return new ResponseEntity<>("Client error: " + ex.getStatusCode() + " - " + ex.getResponseBodyAsString(), HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(HttpServerErrorException.class)
public ResponseEntity handleServerError(HttpServerErrorException ex) {
return new ResponseEntity<>("Server error: " + ex.getStatusCode() + " - " + ex.getResponseBodyAsString(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
// Service that uses RestTemplate to make HTTP calls
@Service
public class ExternalService {
@Autowired
private RestTemplate restTemplate;
public String callExternalService(String url) {
try {
return restTemplate.getForObject(url, String.class);
} catch (HttpClientErrorException | HttpServerErrorException ex) {
throw ex; // Propagate exception for custom handling
}
}
}
// Controller using the external service
@RestController
@RequestMapping("/external")
public class ExternalServiceController {
@Autowired
private ExternalService externalService;
@GetMapping("/data")
public ResponseEntity getExternalData() {
String url = "https://some-external-api.com/resource";
return new ResponseEntity<>(externalService.callExternalService(url), HttpStatus.OK);
}
}
You should catch low-level exceptions (e.g., DataAccessException from Spring Data) in the service layer and rethrow them as service-specific exceptions. This allows you to hide implementation details from the higher layers.
// Custom service-layer exception
public class ServiceException extends RuntimeException {
public ServiceException(String message, Throwable cause) {
super(message, cause);
}
}
// Service layer that catches repository exceptions
@Service
public class MyService {
@Autowired
private MyRepository myRepository;
public MyEntity getEntityById(Long id) {
try {
return myRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Entity not found"));
} catch (DataAccessException ex) {
// Wrap the low-level exception into a service-level exception
throw new ServiceException("Failed to retrieve entity from database", ex);
}
}
}
// Repository layer (Spring Data JPA)
@Repository
public interface MyRepository extends JpaRepository {}
// Controller layer that calls the service
@RestController
@RequestMapping("/entity")
public class MyController {
@Autowired
private MyService myService;
@GetMapping("/{id}")
public ResponseEntity getEntity(@PathVariable Long id) {
return new ResponseEntity<>(myService.getEntityById(id), HttpStatus.OK);
}
// Custom exception handler for service exceptions
@ExceptionHandler(ServiceException.class)
public ResponseEntity handleServiceException(ServiceException ex) {
return new ResponseEntity<>("Service error: " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}