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);
    }
}

3. Designing Error Response Format

FieldPurpose
typeURI identifying error class
titleShort summary
statusHTTP status
detailHuman-readable explanation
instanceURI of failing request
codeApplication error code
traceId/correlationIdFor log correlation
errors[]Field-level errors

4. Implementing Exception Translation

SourceTranslates To
SQLExceptionDataAccessException (Spring)
SocketTimeoutExceptionDownstreamTimeoutException
JsonParseExceptionBadRequestException

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

SeverityUse For
ERRORUnexpected, requires action
WARNDegraded but recovered (e.g., circuit breaker open)
INFOExpected business outcomes (validation)
DEBUGDetailed troubleshooting
Warning: Don't log validation/4xx errors at ERROR — fills logs with noise.

8. Implementing Retry Logic

AspectDetail
RetryableTransient (timeouts, 5xx)
Non-retryable4xx (validation, auth)
BackoffExponential + jitter
Max attempts3-5 typical
Idempotency requiredAvoid 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

FallbackUse Case
Cached valueStale OK
Default valueRecommendations
Queue for laterNon-urgent operations
Degraded UIHide 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

ConventionExample
Snake-case keyorder_not_found
Namespacebilling.card_declined
StableDon't change after release
DocumentedErrors 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(); }
    }
}