Implementing Error Handling
1. Defining Error Response Format
| Standard | Detail |
|---|---|
| RFC 9457 (Problem Details) | Successor to RFC 7807; application/problem+json |
| Fields | type, title, status, detail, instance + extensions |
| Stable code | Machine-readable extension (code) |
Example: Problem Details body
{
"type": "https://api.shop.com/errors/insufficient-funds",
"title": "Insufficient funds",
"status": 402,
"detail": "Account balance is below required amount",
"instance": "/payments/pay_123",
"code": "INSUFFICIENT_FUNDS",
"balance": 12.50,
"required": 49.95
}
2. Using HTTP Status Codes
| Range | Use |
|---|---|
| 4xx | Client error; do not retry blindly |
| 5xx | Server error; safe-to-retry if idempotent |
| 409 | State conflict; reconcile before retry |
| 429 | Throttle; honor Retry-After |
| 503 + Retry-After | Backoff window |
3. Implementing Error Codes
| Practice | Detail |
|---|---|
| Stable strings | INSUFFICIENT_FUNDS, ORDER_NOT_FOUND |
| Namespaced | payments.insufficient_funds |
| Catalog | Documented enumeration |
| Avoid | Numeric only; localized strings |
4. Adding Error Context
| Field | Purpose |
|---|---|
| requestId / traceId | Correlate with logs |
| timestamp | UTC ISO-8601 |
| field errors | Per-field validation messages |
| help URL | Link to docs |
| Avoid | Stack traces in prod responses |
5. Handling Validation Errors
| Element | Detail |
|---|---|
| Status | 400 (syntax) / 422 (semantics) |
| Errors array | Per-field code, message, pointer |
| JSON Pointer | RFC 6901 path: /items/0/qty |
| i18n | Code = key; localize in client |
6. Implementing Global Error Handlers
Example: Spring Boot 3 @RestControllerAdvice
@RestControllerAdvice
class GlobalErrors {
@ExceptionHandler(EntityNotFoundException.class)
ProblemDetail notFound(EntityNotFoundException ex) {
var p = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
p.setProperty("code", "NOT_FOUND");
return p;
}
@ExceptionHandler(MethodArgumentNotValidException.class)
ProblemDetail validation(MethodArgumentNotValidException ex) {
var p = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
p.setProperty("errors", ex.getFieldErrors().stream()
.map(f -> Map.of("field", f.getField(), "code", f.getCode())).toList());
return p;
}
}
7. Logging Errors
| Practice | Detail |
|---|---|
| Structured | JSON with stable keys |
| Include | traceId, userId, errorCode, exception class |
| Stack trace | Server-side only |
| PII | Mask before logging |
| Level | WARN (expected) / ERROR (unexpected) |
8. Propagating Errors
| Concern | Practice |
|---|---|
| Translate | Wrap downstream errors with own code |
| Don't leak internals | Strip stack/SQL from public response |
| Preserve cause | Log original; expose generic to client |
| gRPC ↔ HTTP | Map status codes consistently |
9. Implementing Fallback Responses
| Type | Example |
|---|---|
| Cached | Last successful payload |
| Static default | Empty list, defaults |
| Degraded data | Subset of fields |
| Indicator | Flag in response ("stale": true) |
10. Handling Timeout Errors
| Action | Detail |
|---|---|
| 504 Gateway Timeout | Upstream deadline exceeded |
| Cancel work | Propagate cancellation token |
| Retry? | Only if idempotent; with backoff |
| Alert | Spike in 504s = capacity issue |
11. Implementing Retry Logic
| Setting | Default |
|---|---|
| Max attempts | 3 |
| Initial delay | 100–250 ms |
| Multiplier | 2.0 |
| Jitter | ±50% |
| Retry on | Network, 5xx (idempotent), 429 + Retry-After |
12. Using Error Aggregation
| Tool | Use |
|---|---|
| Sentry / Bugsnag / Rollbar | Group similar errors, alerting |
| Honeycomb | High-cardinality query of errors |
| OpenTelemetry | Errors as span events |
| Slack/PagerDuty | Alert routing |