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 failure
  • details — array of field-level errors for validation failures
  • docsUrl — link to documentation that explains the error and how to fix it

Error Code Design

Error codes are the stable contract. Version them carefully:

PatternExampleWhy
Domain prefixAUTH_TOKEN_EXPIREDImmediately clear which subsystem produced the error
Specific over genericUSER_EMAIL_TAKENvs CONFLICT — specific code means client can show right message without parsing
Never use integersRATE_LIMIT_EXCEEDEDvs 1042 — integers are opaque, change meaning across API versions, break without docs
Additive onlyNEW_CODE_IN_V2Removing 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
  }
}
StatusRetry?Strategy
400, 401, 403, 404, 422NoClient must fix the request first — retrying won't help
408 Request TimeoutYesImmediate retry with idempotency key
429 Too Many RequestsYesExponential back-off, respect Retry-After header
500 Server ErrorConditionalOnly for idempotent requests (GET, PUT, DELETE)
502, 503, 504YesExponential back-off with jitter; respect Retry-After if present

What NOT to Leak in Error Responses

🔒 Security: Never Expose Internals
  • 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.

✅ Error Handling Checklist
  • One error envelope shape used consistently across all endpoints
  • Machine-readable code field (SCREAMING_SNAKE_CASE string)
  • requestId in every error response, correlated to server logs
  • All validation errors returned at once (not one per response)
  • Retry-After header 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