DEV Community

Cover image for ๐ŸŒŸ Modern Global Exception Handling in Spring Boot
AYON KARMAKAR
AYON KARMAKAR

Posted on

๐ŸŒŸ Modern Global Exception Handling in Spring Boot

Exception handling is essential for building robust, user-friendly APIs. With Spring Boot, you can centralize error handling using a Global Exception Handler and custom exceptions for clean, maintainable code. This guide shows you how to set up a modern global exception handler, create a custom exception, and use them in a controllerโ€”all in a way that's easy to understand and quick to implement.


1. Create a Custom Exception: ResourceNotFoundException

A custom exception helps you signal specific error cases (like a missing resource) in your business logic.

// filepath: src/main/java/com/example/exception/ResourceNotFoundException.java
package com.example.exception;

/**
 * Thrown when a requested resource is not found.
 */
public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Implement a Global Exception Handler

Centralize your error handling for all controllers. This example uses a modern response format and logs errors for debugging.

// filepath: src/main/java/com/example/exception/GlobalExceptionHandler.java
package com.example.exception;

import com.example.utils.ApiResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    // Handle Resource Not Found exceptions
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ApiResponse<Object>> handleResourceNotFound(ResourceNotFoundException ex) {
        ApiResponse<Object> response = ApiResponse.error(ex.getMessage(), "RESOURCE_NOT_FOUND");
        return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
    }

    // Handle validation errors
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiResponse<List<Map<String, String>>>> handleValidation(MethodArgumentNotValidException e) {
        List<Map<String, String>> details = e.getBindingResult()
            .getAllErrors()
            .stream()
            .map(error -> {
                Map<String, String> errorDetail = new HashMap<>();
                errorDetail.put("field", ((org.springframework.validation.FieldError) error).getField());
                errorDetail.put("message", error.getDefaultMessage());
                return errorDetail;
            })
            .collect(Collectors.toList());

        ApiResponse.Meta meta = new ApiResponse.Meta(
            Instant.now(),
            UUID.randomUUID().toString(),
            "1.0"
        );

        ApiResponse<List<Map<String, String>>> response = new ApiResponse<>(
            "fail",
            "Validation error!",
            details,
            meta,
            null
        );

        return new ResponseEntity<>(response, HttpStatus.UNPROCESSABLE_ENTITY);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<Object>> handleGenericException(Exception exception) {
        log.error("Internal Server Error: {}", exception.getMessage(), exception);
        ApiResponse<Object> response = ApiResponse.error(exception.getMessage(), "INTERNAL_SERVER_ERROR");
        return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Example Controller Using the Exception

Hereโ€™s a simple controller method that demonstrates how the custom exception and global handler work together.

Note: Service logic is placed in the controller for demonstration.

Best Practice: Move business logic to a service class for maintainability.

// filepath: src/main/java/com/example/controller/CategoryController.java
package com.example.controller;

import com.example.entity.Category;
import com.example.exception.ResourceNotFoundException;
import com.example.utils.ApiResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Arrays;
import java.util.List;

@RestController
@RequestMapping("/api/v1/categories")
public class CategoryController {

    // Example GET endpoint with inline logic for demonstration.
    // Best Practice: Move this logic to a service class!
    @GetMapping("/{id}")
    public ResponseEntity<ApiResponse<Category>> getCategoryById(@PathVariable Long id) {
        List<Category> categories = Arrays.asList(
            new Category(1L, "Fruits", "Fresh fruits", true, "/images/fruits.png"),
            new Category(2L, "Vegetables", "Green veggies", false, "/images/veggies.png")
        );
        Category found = categories.stream()
            .filter(cat -> cat.getId().equals(id))
            .findFirst()
            .orElseThrow(() -> new ResourceNotFoundException("Category not found with id: " + id));

        return ResponseEntity.ok(ApiResponse.success(found, "Category retrieved successfully"));
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Sample Error Response

When a category is not found, the API returns:

{
  "status": "fail",
  "message": "Validation error!",
  "data": [
    { "field": "name", "message": "Category name is required" }
  ],
  "meta": {
    "timestamp": "2025-09-17T12:34:56Z",
    "requestId": "b1a2c3d4-e5f6-7890-abcd-ef1234567890",
    "version": "1.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

5. Best Practices

  • Centralize exception handling with @RestControllerAdvice.
  • Use custom exceptions for clear, domain-specific errors.
  • Keep business logic in service classes for clean controllers.
  • Return structured error responses for easy client integration.
  • Log errors for debugging and monitoring.

Conclusion

With a global exception handler and custom exceptions, your Spring Boot API will be easier to maintain, debug, and consume.

Start with the patterns above and adapt them to your project for a modern, professional backend!


If you found this guide helpful, share it with your team or star your repository!

Top comments (0)