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:


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> handleValidationExceptions(MethodArgumentNotValidException ex) { Map errors = new HashMap<>(); ex.getBindingResult().getAllErrors().forEach((error) -> { String fieldName = ((FieldError) error).getField(); String errorMessage = error.getDefaultMessage(); errors.put(fieldName, errorMessage); }); return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST); } }

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

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);
    }
}