One should try to predicts the failure points as much as possible at-least while programming
graph LR A[Errors In Controller] --> B[catched by controller Advice] A --> A2[Type Of Error] A2 --> E1[Business Logic Error] A2 --> E2[Server Error eg db, redis, client server error ] E1 --> E11[Directly Send to user mostly no need for alert] E2 --> E21[Need to setup alert as this is system's fault]
🧾 Error Classification & HTTP Status Codes
🔹 1. Business Errors (4xx)
Errors due to invalid client input or known domain logic.
| Type | HTTP Code | Notes |
|---|---|---|
| Validation Failed | 400 | Input field errors |
| Missing or Invalid Params | 400 | Query/body/path issues |
| Unauthorized | 401 | Auth token missing/invalid |
| Forbidden | 403 | Authenticated but not authorized |
| Resource Not Found | 404 | Entity does not exist |
| Conflict / Duplicate | 409 | Already exists (e.g. email taken) |
| Rate Limit Exceeded | 429 | Too many requests |
→ No alert needed. Log as INFO or WARN.
🔹 2. System Errors (5xx)
Failures due to infra, network, or unhandled conditions.
| Type | HTTP Code | Notes |
|---|---|---|
| Internal Server Error | 500 | Catch-all for unhandled exceptions |
| Database Failure | 500 | Connectivity/query failure |
| External Service Failure | 502/504 | Downstream API failed / timeout |
| Dependency Timeout | 504 | Redis/DB/service timeout |
→ Alert required. Log as ERROR. Include traceId.
🔹 3. Custom Recommendation: can be implemented Further
-
Always return structured JSON:
{ "status": 500, "error": "Internal Server Error", "message": "Database unavailable", "traceId": "abc123" } -
Include
traceId(MDC/Sleuth) for log correlation.
Code
GlobalExceptionHandler
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponseDto> handleGenericException(
Exception ex, WebRequest request) {
logger.error("Unexpected error occurred: ", ex);
logger.error("Caught in global: {}", ex.getClass().getSimpleName(), ex);
ErrorResponseDto errorResponse = new ErrorResponseDto(
"An unexpected error occurred ",
"INTERNAL_SERVER_ERROR",
HttpStatus.INTERNAL_SERVER_ERROR.value());
return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponseDto> handleUserNotFoundException(
UserNotFoundException ex, WebRequest request) {
logger.warn("User not found: {}", ex.getMessage());
ErrorResponseDto errorResponse = new ErrorResponseDto(
ex.getMessage(),
"USER_NOT_FOUND",
HttpStatus.NOT_FOUND.value());
return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(UserAlreadyExistsException.class)
public ResponseEntity<ErrorResponseDto> handleUserAlreadyExistsException(
UserAlreadyExistsException ex, WebRequest request) {
logger.warn("User already exists: {}", ex.getMessage());
ErrorResponseDto errorResponse = new ErrorResponseDto(
ex.getMessage(),
"USER_ALREADY_EXISTS",
HttpStatus.CONFLICT.value());
return new ResponseEntity<>(errorResponse, HttpStatus.CONFLICT);
}
@ExceptionHandler(InvalidParamsProvidedException.class)
public ResponseEntity<ErrorResponseDto> handleInValidParamsErrorException(InvalidParamsProvidedException ex,
WebRequest request) {
logger.warn("Invalid Email Provided : {}", ex.getMessage());
ErrorResponseDto errorResponse = new ErrorResponseDto(
ex.getMessage(),
"INVALID_PARAMS",
HttpStatus.UNAUTHORIZED.value());
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}
}
Error Definition
package io.saanvi.saanvibackend.core.exception.common;
public class InvalidParamsProvidedException extends RuntimeException {
public InvalidParamsProvidedException(String message) {
super(message);
}
}
How to use
public User findUserById(String Id) throws Exception {
Optional<User> user = userRepository.findById(Id);
if (user.isEmpty())
throw new UserNotFoundException("User Not Found");
return user.get();
}