Guides

Error Handling

Best practices for handling API errors in your application.

Error shape

Errors are always JSON with an error string; validation failures also include a details array with one message per invalid field:

json
// 400 — validation
{ "error": "Validation failed", "details": ["email is required", "password must be at least 8 characters"] }

// 401 — auth
{ "error": "Invalid token" }

// 429 — rate limit
{ "error": "API key rate limit exceeded" }
// 400 — validation
{ "error": "Validation failed", "details": ["email is required", "password must be at least 8 characters"] }

// 401 — auth
{ "error": "Invalid token" }

// 429 — rate limit
{ "error": "API key rate limit exceeded" }

See Error Explorer for the complete list of status codes and error messages by endpoint.

What to retry, and how

Retry 429 and 5xx — these are usually transient. Don't retry 400/401/403/404 without changing the request first — the same request will fail the same way every time.

javascript
async function requestWithRetry(url, options, maxAttempts = 3) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const res = await fetch(url, options);
    if (res.ok) return res.json();

    const retryable = res.status === 429 || res.status >= 500;
    if (!retryable || attempt === maxAttempts) {
      const body = await res.json().catch(() => ({}));
      throw new Error(body.error || `Request failed: ${res.status}`);
    }

    // Exponential backoff with jitter: 1s, 2s, 4s...
    const delay = 2 ** (attempt - 1) * 1000 + Math.random() * 250;
    await new Promise((resolve) => setTimeout(resolve, delay));
  }
}
async function requestWithRetry(url, options, maxAttempts = 3) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const res = await fetch(url, options);
    if (res.ok) return res.json();

    const retryable = res.status === 429 || res.status >= 500;
    if (!retryable || attempt === maxAttempts) {
      const body = await res.json().catch(() => ({}));
      throw new Error(body.error || `Request failed: ${res.status}`);
    }

    // Exponential backoff with jitter: 1s, 2s, 4s...
    const delay = 2 ** (attempt - 1) * 1000 + Math.random() * 250;
    await new Promise((resolve) => setTimeout(resolve, delay));
  }
}

Handling 401 specifically

A 401 from an end-user Bearer token usually means the token expired, was revoked (password change, logout), or is otherwise invalid — refresh it once using the user's refreshToken and retry the original request before falling back to signing the user out. See User Authentication.

A 401 from an API key almost always means the key is wrong, deleted, or expired — there's no refresh flow for keys; generate a new one from the Console.

Tip
Log the full error body (not just the status code) during development — the details array on a 400 usually tells you exactly which field is wrong, without needing to guess from the status code alone.