Spring Boot Global Exception Handling with Custom and System Exceptions

Custom Exception Class

A custom exception extends RuntimeException to carry structured error details such as error codes and messages.

@Data
public class BusinessException extends RuntimeException {

    private String code;
    private String message;

    public BusinessException() {
        super();
    }

    public BusinessException(String code, String message) {
        super(message);
        this.code = code;
        this.message = message;
    }

    public BusinessException(String code, String message, Throwable cause) {
        super(message, cause);
        this.code = code;
        this.message = message;
    }
}

Global Exception Handler

Using @RestControllerAdvice, a global handler intercepts different exception types and returns standardized JSON responses.

@Slf4j
@RestControllerAdvice
public class GlobalErrorController {

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ApiResponse> handleBusinessException(BusinessException ex) {
        log.warn("Business exception caught: {}", ex.getMessage());
        return ResponseEntity.badRequest().body(
            ApiResponse.failure(ex.getCode(), ex.getMessage(), null)
        );
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiResponse> handleValidationException(MethodArgumentNotValidException ex) {
        log.warn("Validation failed: {}", ex.getMessage());
        String errorMessage = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(error -> error.getDefaultMessage())
            .collect(Collectors.joining("; "));
        
        return ResponseEntity.badRequest().body(
            ApiResponse.failure("VALIDATION_ERROR", errorMessage, null)
        );
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse> handleSystemException(Exception ex) {
        log.error("Unexpected system error", ex);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(
            ApiResponse.failure("SYSTEM_ERROR", "An internal error occurred.", null)
        );
    }
}

Response Structure

The API response format is encapsulated in a reusable class to ensure uniformity.

@Data
public class ApiResponse<T> {

    private String status;
    private String code;
    private String message;
    private T data;
    private LocalDateTime timestamp;

    private ApiResponse() {
        this.timestamp = LocalDateTime.now();
    }

    public static <T> ApiResponse<T> success(T data) {
        ApiResponse<T> response = new ApiResponse<>();
        response.status = "SUCCESS";
        response.code = "OK";
        response.message = "Operation completed successfully";
        response.data = data;
        return response;
    }

    public static <T> ApiResponse<T> failure(String code, String message, T data) {
        ApiResponse<T> response = new ApiResponse<>();
        response.status = "FAILURE";
        response.code = code;
        response.message = message;
        response.data = data;
        return response;
    }
}

Error Code Enum

Centralized error codes improve consistency and readability.

public enum ErrorCode {

    OK("OK", "Success"),
    VALIDATION_ERROR("VALIDATION_ERROR", "Input validation failed"),
    BUSINESS_ERROR("BUSINESS_ERROR", "Business rule violation"),
    SYSTEM_ERROR("SYSTEM_ERROR", "Internal server error");

    private final String code;
    private final String description;

    ErrorCode(String code, String description) {
        this.code = code;
        this.description = description;
    }

    public String getCode() { return code; }
    public String getDescription() { return description; }
}

Request Validation Model

Use Jakarta Bean Validation annotations to enforce input constraints.

@Data
public class UserRequest {

    @NotNull(message = "Username cannot be null")
    private String username;

    @Size(max = 50, message = "Description must not exceed 50 characters")
    private String description;
}

Controller Endpoints

Sample controller demonstrating various error scenarios.

@RestController
@RequestMapping("/api/test")
public class TestController {

    @PostMapping("/validate")
    public ApiResponse<String> validateInput(@Valid @RequestBody UserRequest request) {
        return ApiResponse.success("Validation passed: " + request.getUsername());
    }

    @GetMapping("/divide-by-zero")
    public ApiResponse<String> triggerArithmeticError() {
        int result = 1 / 0; // Will trigger ArithmeticException
        return ApiResponse.success("Result: " + result);
    }

    @PostMapping("/trigger-business-error")
    public ApiResponse<String> triggerCustomError() {
        if (true) {
            throw new BusinessException(ErrorCode.BUSINESS_ERROR.getCode(), "Simulated business failure");
        }
        return ApiResponse.success("No error");
    }
}

Test Scenarios

  • Scenario 1: Sending a request with an empty username triggers a validation error.
  • Scenario 2: Accessing /divide-by-zero trigggers a system-level ArithmeticException.
  • Scenario 3: Calling /trigger-business-error throws a custom BusinessException.

Each scanario is intercepted by the global handler, returning a consistent JSON response with appropriate HTTP status codes, error codes, and messages. This structure simplifies client-side error handling and enibles efficient debugging.

Tags: SpringBoot RestControllerAdvice GlobalExceptionHandler BusinessException MethodArgumentNotValidException

Posted on Sun, 17 May 2026 14:20:13 +0000 by varghesedxb