API Error Handling and Retry Strategy: Give Your API an Error Language People Can Read

An API error is the most common conversation your backend will have with the outside world, yet many sites treat error handling like a black box: a raw HTML error page, or a bare 500 with no context, leaving the caller to guess. The problem is rarely a single status code — it is the absence of a consistent convention. A well-designed error response tells the caller three things at a glance: which layer failed, why it failed, and whether retrying will help.

Layer Your Errors: Client Fault or Server Fault

Status Meaning Common Triggers Safe to Retry
400 Malformed request JSON parse failure, missing required field No
401/403 Unauthenticated / forbidden Expired token, bad signature No (re-send after refresh)
404 Resource not found Wrong path, deleted resource No
422 Semantic validation failed Invalid email, insufficient stock No
429 Rate limited Over quota or frequency limit Yes (honor Retry-After)
500/502/503/504 Server-side failure Crash, gateway timeout, upstream down Yes (bounded)

A Consistent Error Response Shape

Both 4xx and 5xx responses should use the same JSON structure instead of ad-hoc text.

{
  "code": "RATE_LIMIT",
  "message": "too many requests",
  "request_id": "abc123",
  "retry_after": 3,
  "details": {"limit": 100, "window": "1m"}
}
  • code — machine-readable error code the client can branch on
  • message — human-readable explanation
  • request_id — the key that ties a response to your logs
  • retry_after — how many seconds the client should wait (for 429); far more accurate than the client guessing

Retry Strategy: Not Every Error Deserves a Retry

Retrying is not "call again when it fails." Unbounded retries turn a small fault into an avalanche — the busier the server gets, the harder clients hammer it. Follow two rules: only retry recoverable errors, and always wait between attempts.

Parameter Suggested Value Notes
Retryable errors 429, 502, 503, 504 Never retry 400/401/403/404/422
Initial wait 1s Starting point for exponential backoff
Backoff factor 2 1s → 2s → 4s → 8s
Max wait 30–60s Keep the ceiling bounded
Retry limit 3 attempts Beyond that, fail fast and escalate
Jitter ±20% random Prevents synchronized retry storms

Adding jitter to exponential backoff is a tiny change that measurably reduces retry storms. Clients should also honor the Retry-After header from the server — it is more accurate than anything computed locally.

Idempotency Keys: Retry Without Double Ordering

The biggest risk with retrying POST requests is side effects running twice — a user taps "Place order" twice and ends up with two orders. The fix is an idempotency key: the client sends a unique value in a header, the server remembers it, and repeated requests with the same key return the first result.

Idempotency-Key: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d

A Real-World Scenario: Three Failed Payment Callbacks

A payment callback endpoint started returning 502s one afternoon. The provider's retry mechanism fired three times, all failed, and the jobs landed in a dead-letter queue. The on-call engineer followed the same request_id through the logs and traced it to the gateway — one upstream machine had run out of memory. Because the error shape was uniform and request_id was everywhere, the root cause took about fifteen minutes to find. With messy ad-hoc error text, that investigation would have taken at least three times as long.

What Else the Client Should Do

  • Encapsulate retries in the gateway or SDK layer; do not scatter for loops across business code
  • Add a circuit breaker: after a threshold of consecutive failures (e.g., 5), fail fast for a short window instead of sending requests
  • Separate connect and read timeouts (default 5s/10s) so a slow endpoint does not stall the whole page

Error Code Naming and API Documentation Conventions

A consistent structure is not enough; the codes themselves should be readable. Name them as "domain + scenario + state," e.g. ORDER_INSUFFICIENT_STOCK, PAYMENT_CARD_DECLINED, AUTH_TOKEN_EXPIRED — far easier to reason about than a bare number like 1001. Once a code ships, keep it stable; clients typically hard-code branches on specific codes, and renaming breaks compatibility.

Document every error code in your API docs: what triggers it, the message template, whether it is retryable, and whether a retry_after applies. Generate a single "error code reference" that both frontend and backend treat as the source of truth. In practice, many teams put the error-code table at the top of the OpenAPI spec and add automated tests asserting that every 4xx returns the same shape and carries a request_id.

One more convention that is often skipped: tie logs to errors. Every error response should let you trace from the request_id back to backend logs, so a screenshot of a user's error can be investigated quickly. Inject request_id in an entry middleware, propagate it downstream, and you get a complete call chain.

Reference: Stripe API error handling https://docs.stripe.com/api/errors
Reference: Google Cloud API error model https://cloud.google.com/apis/design/errors
Reference: RFC 6585 (429 Too Many Requests) https://www.rfc-editor.org/rfc/rfc6585