🌱 Spring Boot Q54 / 69

How to write custom exceptions in spring boot?

AI-Powered Answer ✓ Answered

Custom exceptions in Spring Boot provide a structured way to handle specific error conditions within your application. They allow you to define distinct error types, which can then be mapped to appropriate HTTP status codes and response bodies, improving API clarity and maintainability compared to generic error handling.

1. Define a Custom Exception Class

Custom exceptions typically extend RuntimeException (for unchecked exceptions) or Exception (for checked exceptions). For most Spring Boot REST APIs, RuntimeException is preferred as it avoids cluttering method signatures with throws clauses. Implement standard constructors to provide a message and, optionally, a cause.

java
package com.example.demo.exceptions;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

// Optional: Can directly set the HTTP status code
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {

    public ResourceNotFoundException(String message) {
        super(message);
    }

    public ResourceNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
}

2. Create a Global Exception Handler (ControllerAdvice)

Use Spring's @ControllerAdvice annotation to create a global exception handler. This class can contain @ExceptionHandler methods that intercept specific exception types thrown anywhere in your controllers. These methods allow you to customize the HTTP response, including the status code and response body.

java
package com.example.demo.handlers;

import com.example.demo.exceptions.ResourceNotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<Object> handleResourceNotFoundException(
            ResourceNotFoundException ex, WebRequest request) {

        Map<String, Object> body = new HashMap<>();
        body.put("timestamp", LocalDateTime.now());
        body.put("message", ex.getMessage());
        body.put("description", request.getDescription(false));

        return new ResponseEntity<>(body, HttpStatus.NOT_FOUND);
    }

    // You can add more @ExceptionHandler methods for other custom exceptions or generic ones
    @ExceptionHandler(Exception.class)
    public ResponseEntity<Object> handleAllUncaughtException(
            Exception ex, WebRequest request) {

        Map<String, Object> body = new HashMap<>();
        body.put("timestamp", LocalDateTime.now());
        body.put("message", "An unexpected error occurred.");
        body.put("details", ex.getMessage()); // For debugging, might hide in production
        body.put("description", request.getDescription(false));

        return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

3. Throw the Custom Exception

Once your custom exception and global handler are in place, you can throw the exception from any service layer, repository, or controller method when the specific error condition is met. Spring will automatically route it to the appropriate @ExceptionHandler.

java
package com.example.demo.controllers;

import com.example.demo.exceptions.ResourceNotFoundException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/products")
public class ProductController {

    // Imagine a ProductService that fetches products
    // private final ProductService productService;

    @GetMapping("/{id}")
    public String getProductById(@PathVariable Long id) {
        // Simulate finding a product or not
        if (id % 2 != 0) { // Example condition: if ID is odd, not found
            throw new ResourceNotFoundException("Product with ID " + id + " not found.");
        }
        return "Product details for ID: " + id; // Replace with actual product data
    }
}

4. Considerations and Best Practices

  • Unchecked vs. Checked Exceptions: For most web applications, extending RuntimeException is preferred to avoid boilerplate throws declarations.
  • Specific Error Payloads: Design a consistent error response structure (e.g., including timestamp, error message, and a unique error code).
  • Logging: Ensure exceptions are properly logged in your @ExceptionHandler for debugging and monitoring, but be careful not to expose sensitive information to clients.
  • @ResponseStatus vs. @ExceptionHandler: @ResponseStatus can be used directly on the exception class for simple cases where only the HTTP status code needs to be set. For more complex response bodies or conditional logic, @ExceptionHandler in a @ControllerAdvice is more powerful.