REST API Error Handling Best Practices: A Practical, Modern Guide
A practical guide to REST API error handling: status codes, structured responses (RFC 7807), retries, rate limits, idempotency, security, and observability.
Image used for representation purposes only.
Why Error Handling Matters in REST APIs
Robust error handling is the difference between APIs that feel effortless and those that frustrate. Clear, consistent errors shorten integration time, reduce support tickets, and make production incidents easier to diagnose. In REST, the transport already gives you a language—HTTP status codes and headers—so the goal is to use them precisely and pair them with a predictable, machine-readable body.
This guide distills practical patterns for designing, documenting, and operating error responses that scale with your product and your teams.
Core Principles
- Use the HTTP layer first. Let status codes communicate the broad category (2xx success, 4xx client error, 5xx server error). Use headers to convey semantics like retry timing.
- Be consistent. Return the same structure for every error, across all endpoints and versions.
- Be explicit and minimal. Include only what clients need to recover or report a bug. Never leak stack traces, SQL, environment variables, or internal hostnames.
- Be machine- and human-friendly. Provide a stable error code for automation and a concise message for humans. Localize the message, not the code.
- Prefer standards. Adopt “Problem Details for HTTP APIs” (RFC 7807) as your base schema and extend it carefully.
- Optimize for operability. Correlate requests end-to-end, log structured errors, and capture metrics that tie to SLOs.
Choosing the Right HTTP Status Codes
Pick the narrowest, accurate code. Common mappings:
- 400 Bad Request: malformed JSON, invalid query syntax.
- 401 Unauthorized: no/invalid credentials. Include WWW-Authenticate if relevant.
- 403 Forbidden: authenticated but not allowed.
- 404 Not Found: resource absent or not visible to caller. Avoid leaking existence of protected resources.
- 405 Method Not Allowed: wrong method; return Allow header with supported methods.
- 406 Not Acceptable: unsupported Accept header; list supported media types.
- 409 Conflict: state conflicts (e.g., duplicate unique key, version mismatch).
- 410 Gone: resource intentionally removed and not coming back.
- 415 Unsupported Media Type: wrong Content-Type.
- 422 Unprocessable Entity: syntactically valid but semantically invalid payload (common for validation errors).
- 429 Too Many Requests: throttling/rate limit. Include Retry-After.
- 500 Internal Server Error: unexpected server-side fault.
- 501 Not Implemented: endpoint or method unimplemented.
- 503 Service Unavailable: temporary outage or maintenance; include Retry-After.
- 504 Gateway Timeout: upstream dependency timeout.
Standardize Your Error Body (RFC 7807 + Extensions)
Use a single error envelope for all 4xx/5xx responses. RFC 7807 defines a portable shape and vocabulary. Extend only with fields that help remediation.
Example (application/problem+json):
{
"type": "https://api.example.com/problems/validation-error",
"title": "Invalid request parameters",
"status": 422,
"detail": "One or more fields failed validation.",
"instance": "/orders/123/checkout",
"code": "VALIDATION_FAILED",
"correlationId": "b4f9b5f0-9a4e-44a0-a7bb-3a2b8a4fe1ac",
"errors": [
{ "name": "email", "reason": "must be a valid email" },
{ "name": "items[0].quantity", "reason": "must be >= 1" }
],
"retryable": false,
"docs": "https://docs.example.com/errors#VALIDATION_FAILED"
}
Notes:
- type is a stable identifier (prefer a URL you control). It can host human docs.
- code is your internal, stable error code for automation and dashboards.
- correlationId traces a request across services; return it on success too.
- errors array pinpoints field-level issues for validation-heavy endpoints.
- retryable flags whether clients should retry. Pair this with headers for timing.
Validation Errors: Be Precise and Actionable
Validation is where developers most often interact with errors. Tips:
- Prefer 422 Unprocessable Entity when the JSON is well-formed but invalid. Use 400 for malformed input.
- Include per-field errors with explicit paths (e.g., items[0].quantity). If you use JSON Pointer, be consistent (e.g., /items/0/quantity).
- Keep messages short and deterministic; avoid varying punctuation or phrasing across locales. Localize separately if needed.
Example:
{
"type": "https://api.example.com/problems/validation-error",
"title": "Invalid request parameters",
"status": 422,
"code": "VALIDATION_FAILED",
"errors": [
{ "path": "/items/0/quantity", "reason": "must be >= 1" },
{ "path": "/shipping/address/postalCode", "reason": "invalid format" }
]
}
Authentication and Authorization Failures
- 401 Unauthorized: missing/expired/invalid token. Include a WWW-Authenticate header that hints the scheme and realm.
- 403 Forbidden: token valid but lacks permission or entitlements.
Example body (401):
{
"type": "https://api.example.com/problems/authentication",
"title": "Authentication required",
"status": 401,
"code": "AUTH_REQUIRED",
"detail": "Bearer token missing or expired"
}
Concurrency, Conflicts, and Idempotency
- Use ETag with If-Match for optimistic concurrency. If the client’s ETag is stale, return 412 Precondition Failed.
- Return 409 Conflict for business-rule clashes (e.g., duplicate order) with a clear code like DUPLICATE_RESOURCE.
- Make unsafe operations idempotent where possible. Accept an Idempotency-Key header on POST; for duplicates, return the original response (same status and body). Document idempotency window and storage semantics.
Example conflict:
{
"type": "https://api.example.com/problems/conflict",
"title": "Resource conflict",
"status": 409,
"code": "DUPLICATE_EMAIL",
"detail": "A user with that email already exists"
}
Retries, Backoff, and Time-bound Guidance
- Mark transient faults with 5xx. If you know a request is safe to retry (e.g., GET, DELETE with idempotency), set retryable: true.
- Use Retry-After on 429 and 503 to communicate wait time (seconds or HTTP-date).
- Encourage exponential backoff with jitter. Document examples for SDK authors.
Example headers:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1710783600
Rate Limiting and Quotas
- 429 Too Many Requests signals throttling; use a consistent scheme across endpoints.
- Return headers that expose policy and state: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset (epoch seconds). If you adopt an alternate naming, keep it stable.
- For per-user vs. per-token limits, document the scope clearly.
Content Negotiation and Media Types
- Validate Accept and Content-Type early. Respond with 406 Not Acceptable or 415 Unsupported Media Type as appropriate.
- Prefer application/json; when using RFC 7807, set Content-Type: application/problem+json for error bodies.
- Versioning via media types is acceptable; reflect that in 406 messages.
Observability: Correlation, Logging, and Metrics
- Generate a correlationId for every request (e.g., UUID v4). Return it in an X-Correlation-Id header and echo it in problem responses. Propagate it through all internal calls.
- Log structured events (JSON) with fields: timestamp, level, service, route, method, status, correlationId, code, userId/tenant (if safe), latency, and sanitized context.
- Emit metrics by category: error.count{code}, error.rate{status}, p95 latency, and retry_after.seconds. Tie them to SLOs and alert on burn rates.
Documentation and Contracts
- Define error schemas in OpenAPI components; reference them in responses. Provide realistic, copy-pasteable examples.
- Document every stable code and its remediation steps. Keep human-friendly docs at the type URL if you use RFC 7807.
- Include header behavior (WWW-Authenticate, Retry-After, Allow, X-RateLimit-*), idempotency semantics, and concurrency control in your docs.
OpenAPI snippet:
components:
schemas:
Problem:
type: object
required: [type, title, status]
properties:
type: { type: string, format: uri }
title: { type: string }
status: { type: integer, format: int32 }
detail: { type: string }
instance: { type: string }
code: { type: string }
correlationId: { type: string }
errors:
type: array
items:
type: object
properties:
path: { type: string }
reason: { type: string }
Internationalization and Accessibility
- Keep code and type English-only and stable. Localize title/detail based on Accept-Language, or provide a locale query parameter if necessary.
- Keep messages concise (one actionable sentence). Avoid jargon and absolutes. Screen-reader-friendly phrasing improves accessibility.
Security Considerations
- Never expose stack traces, SQL, file paths, memory addresses, or dependency versions.
- Normalize timing and wording for auth errors to avoid user enumeration (e.g., same 401 for wrong user vs. wrong password).
- Rate-limit sensitive endpoints (login, token exchange) and return 429 uniformly.
- Sanitize echo fields and truncate overly long inputs in error echoes to prevent log injection.
Async and Long-running Operations
- For operations that take time, return 202 Accepted with a status URL in Location. Surface errors at the status resource using the same problem shape.
- Use Retry-After on 202 if polling should be delayed. For webhooks, sign payloads and include correlationId.
Versioning and Compatibility
- Treat error codes as part of your public contract. Adding new codes is fine; do not repurpose existing codes.
- If you change the error shape, bump the API version or media type and support both for a deprecation window.
Do’s and Don’ts
Do:
- Use specific HTTP codes and consistent error bodies.
- Include correlationId, stable code, and clear remediation hints.
- Provide Retry-After and rate-limit headers when applicable.
- Test errors as first-class citizens (contract tests, chaos/fault injection).
Don’t:
- Return 200 OK with an error payload.
- Leak internals or vary messages unpredictably.
- Overload a single code (e.g., using 400 for everything).
- Force clients to parse free-text to differentiate errors.
End-to-end Example
Request:
POST /v1/orders HTTP/1.1
Content-Type: application/json
Accept: application/json
Idempotency-Key: 4c2a5b5e-0ce2-4b18-8f4e-2a7f3f8c4d01
X-Correlation-Id: 1e2f3a4b-5c6d-7e8f-9012-3456789abcde
{"email":"not-an-email","items":[{"sku":"ABC","quantity":0}]}
Response:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
X-Correlation-Id: 1e2f3a4b-5c6d-7e8f-9012-3456789abcde
{
"type": "https://api.example.com/problems/validation-error",
"title": "Invalid request parameters",
"status": 422,
"code": "VALIDATION_FAILED",
"correlationId": "1e2f3a4b-5c6d-7e8f-9012-3456789abcde",
"errors": [
{ "path": "/email", "reason": "must be a valid email" },
{ "path": "/items/0/quantity", "reason": "must be >= 1" }
],
"retryable": false
}
Checklist for Production Readiness
- Status codes reviewed and mapped for each endpoint.
- One canonical problem+json schema with stable codes.
- Correlation IDs generated, returned, and logged everywhere.
- Rate-limiting and Retry-After implemented and documented.
- Idempotency for POSTs to critical resources.
- Security review: no internal data leakage; uniform auth errors.
- Contract tests for common and edge-case failures.
- Documentation and examples published, with copy-paste snippets.
Conclusion
Good error handling is a product feature. By aligning with HTTP semantics, adopting a standard problem schema, and investing in observability and documentation, you empower client developers to recover quickly, support teams to resolve incidents faster, and your own engineers to ship confidently. Treat errors as part of the API surface, not an afterthought, and you’ll feel the difference in reliability and developer experience.
Related Posts
REST API Pagination: Cursor vs Offset—How to Choose and Implement
A practical guide to REST API pagination—offset vs cursor—with pros/cons, SQL patterns, and implementation tips for scalable, consistent endpoints.
OpenAPI and Swagger: A Practical Tutorial to Document Your REST API
Step-by-step tutorial to design, write, validate, and publish API docs using OpenAPI and Swagger UI, with examples and best practices.
The API Versioning Playbook: Best Practices, Patterns, and Pitfalls
A practical playbook for API versioning: strategies, SemVer, backward compatibility, deprecation, testing, and rollout patterns for stable, evolving APIs.