Client Error Handling

Timeouts, retries, exponential backoff, and user-friendly failures.

On this page

Errors happen in layers

Client-side errors fall into three buckets: network failures, HTTP failures (non-2xx), and application/validation failures. Treat them differently to improve UX and reduce retries.

Network errors

  • DNS issues, offline, timeouts, TLS problems
  • Fetch rejects only for network-level errors

HTTP errors

  • 4xx: client request issue (usually don't retry automatically)
  • 5xx: server issue (retry may be reasonable)
  • 429: rate limited (retry after delay)

Parse errors

Sometimes the response is not JSON (bad proxy, HTML error page). Your client should handle JSON parsing failures gracefully.

A robust fetch helper (pattern)

async function api(url, options = {}) {
  const res = await fetch(url, {
    headers: { "Accept": "application/json", ...(options.headers || {}) },
    ...options
  });

  const text = await res.text();
  const json = text ? (JSON.parse(text) || null) : null;

  if (!res.ok) {
    const msg = json?.detail || json?.title || ("HTTP " + res.status);
    const err = new Error(msg);
    err.status = res.status;
    err.body = json;
    throw err;
  }

  return json;
}

Retries with backoff

Retry only when it makes sense (timeouts, 502/503, sometimes 429). Use exponential backoff with jitter to avoid thundering herds.

Example backoff strategy (concept)

delay = base * 2^attempt + random(0..jitter)

User-friendly error mapping

  • 422: show field-level messages
  • 401: trigger login refresh
  • 403: show “not allowed”
  • 500: show generic message + retry

Checklist

  • Network errors are handled separately from HTTP errors.
  • Non-2xx responses are parsed safely.
  • Retries are limited and use backoff.