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
usernametriggers a validation error. - Scenario 2: Accessing
/divide-by-zerotrigggers a system-levelArithmeticException. - Scenario 3: Calling
/trigger-business-errorthrows a customBusinessException.
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.