How to Handle Exceptions Globally in Spring Boot

How to Handle Exceptions Globally in Spring Boot

Error handling is a fundamental part of building resilient and user-friendly APIs. Instead of repeating try-catch blocks across your controllers and services, Spring Boot provides a clean way to handle exceptions globally using @ControllerAdvice and @ExceptionHandler.

In this post, you’ll learn how to handle exceptions globally in Spring Boot, create custom exception classes, return structured error responses, and apply best practices for 2025.

❌ Why Global Exception Handling Matters

Without proper exception handling:

  • Users receive generic or confusing error messages
  • You leak technical details (stack traces) in production
  • There’s duplicated code everywhere
  • HTTP responses aren’t consistent or helpful

Spring Boot’s approach enables centralized error handling, helping you keep code clean and maintainable.

📦 Project Setup

If you’re starting fresh, include these dependencies:

  • spring-boot-starter-web
  • spring-boot-starter-validation (optional but recommended)

📘 New to Spring Boot? Read Getting Started with Spring Boot 3

🧱 Step 1: Create a Custom Exception

public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}

You can create multiple exception classes to represent different error types (e.g., BadRequestException, UnauthorizedException, etc.).

🎯 Step 2: Create a Global Exception Handler

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ApiError> handleResourceNotFound(ResourceNotFoundException ex) {
        ApiError error = new ApiError(HttpStatus.NOT_FOUND, ex.getMessage());
        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiError> handleGeneralException(Exception ex) {
        ApiError error = new ApiError(HttpStatus.INTERNAL_SERVER_ERROR, "An unexpected error occurred");
        return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

🧾 Step 3: Create an Error Response Model

public class ApiError {
    private int status;
    private String error;
    private LocalDateTime timestamp;

    public ApiError(HttpStatus status, String error) {
        this.status = status.value();
        this.error = error;
        this.timestamp = LocalDateTime.now();
    }

    // Getters and setters omitted for brevity
}

This ensures every error response your API returns has a consistent structure.

🧪 Example Controller

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

    @GetMapping("/{id}")
    public Product getProduct(@PathVariable Long id) {
        // Simulating a missing product
        throw new ResourceNotFoundException("Product with ID " + id + " not found");
    }
}

🧪 Sample Output:

{
  "status": 404,
  "error": "Product with ID 1 not found",
  "timestamp": "2025-05-13T12:01:32.042"
}

📌 Best Practices

  • Return meaningful messages for client errors (400s)
  • Avoid exposing internal stack traces or class names in production
  • Customize responses based on exception type or request path
  • Use logging (e.g., SLF4J) to capture full errors internally
  • Return proper HTTP status codes (e.g., 400, 404, 500)

🔒 Bonus: Handle Validation Exceptions

Add support for @Valid or @Validated request body errors:

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiError> handleValidationError(MethodArgumentNotValidException ex) {
    String errorMsg = ex.getBindingResult().getFieldErrors().stream()
        .map(err -> err.getField() + ": " + err.getDefaultMessage())
        .collect(Collectors.joining(", "));

    ApiError error = new ApiError(HttpStatus.BAD_REQUEST, errorMsg);
    return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}

This helps return clear messages like:

{
  "status": 400,
  "error": "email: must not be blank, age: must be greater than 18"
}

🧠 Final Thoughts

Handling exceptions globally in Spring Boot is clean, scalable, and highly recommended. It improves your API’s consistency, user experience, and overall reliability — especially in production environments.

By using @RestControllerAdvice, custom exceptions, and structured responses, you can handle all edge cases in one place — and keep your controller logic focused on what it should be doing: delivering business functionality.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top