The Universal Error Envelope
Define one error shape and use it everywhere. Consistency is worth more than clever per-endpoint error structures:
{
"error": {
"code": "VALIDATION_FAILED",
"message": "The request body contains invalid data",
"requestId": "req_01HZ3XKJB8N7WVTF9MDQ4RPCE",
"timestamp": "2026-06-07T10:30:00Z",
"details": [
{ "field": "email", "issue": "INVALID_FORMAT", "message": "Must be a valid email address" },
{ "field": "age", "issue": "OUT_OF_RANGE", "message": "Must be between 0 and 150" }
],
"docsUrl": "https://api.example.com/docs/errors#VALIDATION_FAILED"
}
}Each field serves a specific consumer:
code— stable SCREAMING_SNAKE_CASE string for programmatic routing (never an integer)message— human-readable summary for developers; not suitable for end-user display (localise separately)requestId— unique per request; included in server logs so ops can pinpoint the exact failuredetails— array of field-level errors for validation failuresdocsUrl— link to documentation that explains the error and how to fix it
Error Code Design
Error codes are the stable contract. Version them carefully:
| Pattern | Example | Why |
|---|---|---|
| Domain prefix | AUTH_TOKEN_EXPIRED | Immediately clear which subsystem produced the error |
| Specific over generic | USER_EMAIL_TAKEN | vs CONFLICT — specific code means client can show right message without parsing |
| Never use integers | RATE_LIMIT_EXCEEDED | vs 1042 — integers are opaque, change meaning across API versions, break without docs |
| Additive only | NEW_CODE_IN_V2 | Removing or renaming codes is a breaking change — clients switch on them |
Validation Error Pattern
Return all validation errors in a single response — never one error at a time. A form with 5 invalid fields should not require 5 round-trips to discover:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
"error": {
"code": "VALIDATION_FAILED",
"message": "3 fields failed validation",
"requestId": "req_abc123",
"details": [
{
"field": "email",
"issue": "REQUIRED",
"message": "Email is required"
},
{
"field": "password",
"issue": "TOO_SHORT",
"message": "Must be at least 8 characters",
"meta": { "minLength": 8, "actual": 5 }
},
{
"field": "birthDate",
"issue": "FUTURE_DATE",
"message": "Birth date cannot be in the future"
}
]
}
}Retry Signalling
Tell clients exactly when and whether to retry. This prevents thundering herd problems and unnecessary load:
# Rate limited — tell client when to retry
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1749258000
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests. Retry after 60 seconds.",
"retryAfterSeconds": 60
}
}| Status | Retry? | Strategy |
|---|---|---|
| 400, 401, 403, 404, 422 | No | Client must fix the request first — retrying won't help |
| 408 Request Timeout | Yes | Immediate retry with idempotency key |
| 429 Too Many Requests | Yes | Exponential back-off, respect Retry-After header |
| 500 Server Error | Conditional | Only for idempotent requests (GET, PUT, DELETE) |
| 502, 503, 504 | Yes | Exponential back-off with jitter; respect Retry-After if present |
What NOT to Leak in Error Responses
- Stack traces in production responses — log them server-side, never send to clients
- Database error messages (table names, column names, SQL fragments)
- Internal server paths or class names
- Whether a user account exists (use generic "invalid credentials" for auth failures to prevent user enumeration)
- Third-party API keys or tokens embedded in error details
The RFC 7807 Standard (Problem Details)
RFC 7807 "Problem Details for HTTP APIs" is an IETF standard for API error responses with a specific media type (application/problem+json). Its shape is interoperable with any client that understands the standard:
Content-Type: application/problem+json
{
"type": "https://api.example.com/errors/out-of-credit",
"title": "You do not have enough credit",
"status": 403,
"detail": "Your balance is 30, but the cost is 50",
"instance": "/account/transactions/abc123"
}RFC 7807 is a good choice for public APIs that want to be interoperable with industry tooling. Custom envelopes like the one shown earlier are more flexible for complex validation error structures — choose based on your audience and toolchain.
- One error envelope shape used consistently across all endpoints
- Machine-readable
codefield (SCREAMING_SNAKE_CASE string) requestIdin every error response, correlated to server logs- All validation errors returned at once (not one per response)
Retry-Afterheader on 429 responses- No stack traces, DB errors, or internal paths in responses
- Correct HTTP status codes (never 200 for errors)
- Error code documentation URL included in response