Designing Error Handling
1. Creating Custom Exception Hierarchies
Example: Domain exceptions
public abstract class AppException extends RuntimeException {
private final String code;
protected AppException(String code, String msg) { super(msg); this.code = code; }
protected AppException(String code, String msg, Throwable cause) { super(msg, cause); this.code = code; }
public String code() { return code; }
}
public class NotFoundException extends AppException {
public NotFoundException(String entity, Object id) {
super("not_found", entity + " " + id + " not found");
}
}
public class ValidationException extends AppException { ... }
2. Implementing Global Exception Handlers
Example: @RestControllerAdvice
@RestControllerAdvice
public class GlobalErrorHandler {
@ExceptionHandler(NotFoundException.class)
ProblemDetail notFound(NotFoundException e) {
var pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage());
pd.setProperty("code", e.code());
return pd;
}
@ExceptionHandler(MethodArgumentNotValidException.class)
ProblemDetail validation(MethodArgumentNotValidException e) { ... }
@ExceptionHandler(Exception.class)
ProblemDetail unexpected(Exception e) {
log.error("Unhandled", e);
return ProblemDetail.forStatus(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
| Field | Purpose |
| type | URI identifying error class |
| title | Short summary |
| status | HTTP status |
| detail | Human-readable explanation |
| instance | URI of failing request |
| code | Application error code |
| traceId/correlationId | For log correlation |
| errors[] | Field-level errors |
4. Implementing Exception Translation
| Source | Translates To |
| SQLException | DataAccessException (Spring) |
| SocketTimeoutException | DownstreamTimeoutException |
| JsonParseException | BadRequestException |
5. Implementing Circuit Breaker Pattern
CLOSED ── failures > threshold ──> OPEN
↑ │
│ success in HALF_OPEN │ wait period
│ ↓
└──────── HALF_OPEN ←──────────────┘
Example: Resilience4j
@CircuitBreaker(name = "payments", fallbackMethod = "fallback")
public Receipt charge(ChargeRequest req) { return client.charge(req); }
private Receipt fallback(ChargeRequest req, Throwable t) {
return Receipt.queued(req.id());
}
6. Handling Validation Errors
Example: Field error mapping
@ExceptionHandler(MethodArgumentNotValidException.class)
ProblemDetail validation(MethodArgumentNotValidException e) {
var errors = e.getBindingResult().getFieldErrors().stream()
.map(fe -> Map.of("field", fe.getField(), "code", fe.getCode(), "message", fe.getDefaultMessage()))
.toList();
var pd = ProblemDetail.forStatusAndDetail(HttpStatus.UNPROCESSABLE_ENTITY, "validation failed");
pd.setProperty("errors", errors);
return pd;
}
7. Logging Exceptions Appropriately
| Severity | Use For |
| ERROR | Unexpected, requires action |
| WARN | Degraded but recovered (e.g., circuit breaker open) |
| INFO | Expected business outcomes (validation) |
| DEBUG | Detailed troubleshooting |
Warning: Don't log validation/4xx errors at ERROR — fills logs with noise.
8. Implementing Retry Logic
| Aspect | Detail |
| Retryable | Transient (timeouts, 5xx) |
| Non-retryable | 4xx (validation, auth) |
| Backoff | Exponential + jitter |
| Max attempts | 3-5 typical |
| Idempotency required | Avoid duplicate side effects |
Example: Resilience4j retry
@Retry(name = "payments")
public Receipt charge(ChargeRequest r) { return client.charge(r); }
// config: maxAttempts=3, waitDuration=200ms, exponentialBackoffMultiplier=2
9. Designing Fallback Mechanisms
| Fallback | Use Case |
| Cached value | Stale OK |
| Default value | Recommendations |
| Queue for later | Non-urgent operations |
| Degraded UI | Hide non-critical features |
10. Using Result Objects vs Exceptions
Exceptions
- Truly exceptional cases
- Stack traces useful
- Auto-propagation
- Cost: stack capture
Result<T,E>
- Expected business outcomes
- Explicit in signature
- No stack cost
- Forces caller to handle
11. Implementing Error Codes
| Convention | Example |
| Snake-case key | order_not_found |
| Namespace | billing.card_declined |
| Stable | Don't change after release |
| Documented | Errors page in API docs |
12. Implementing Correlation IDs
Example: Servlet filter
public class CorrelationFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
var http = (HttpServletRequest) req;
var id = Optional.ofNullable(http.getHeader("X-Correlation-Id")).orElse(UUID.randomUUID().toString());
MDC.put("correlationId", id);
((HttpServletResponse) res).setHeader("X-Correlation-Id", id);
try { chain.doFilter(req, res); } finally { MDC.clear(); }
}
}