> For the complete documentation index, see [llms.txt](https://developer.abacum.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developer.abacum.ai/guides/handle-errors-and-retries.md).

# Handle errors and retries

One helper that reads every error body, waits on 429, and keeps your token.

Error bodies vary by endpoint. This helper reads every format, so your code handles one error type. See [Errors](/essentials/concepts/errors.md).

It also does four things that every client needs:

* It keeps the access token and requests a new one an hour before it expires. A token lasts 7 days. See [Authenticate](/essentials/authenticate.md).
* It waits the number of seconds in `Retry-After` when it gets `429`, then sends the request again.
* It sends a `GET` again after a `500`, `502`, `503` or `504`. It waits a random time before each retry. The time doubles with each attempt and never exceeds 30 seconds. This is exponential backoff with full jitter.
* It keeps the `X-Request-ID` header of a failed response in the error, so you can give it to support.

{% tabs %}
{% tab title="Python" %}

```python
import os
import random
import time

import requests

BASE_URL = "https://api.abacum.io"
CLIENT_ID = os.environ["ABACUM_CLIENT_ID"]
CLIENT_SECRET = os.environ["ABACUM_CLIENT_SECRET"]

_token = {"value": None, "expires_at": 0.0}


class AbacumError(Exception):
    def __init__(self, status, code, message, request_id=""):
        super().__init__(f"{status} {code}: {message} (request {request_id})")
        self.status = status
        self.code = code
        self.message = message
        self.request_id = request_id


def get_token():
    """Return a cached token, and request a new one an hour before it expires."""
    if _token["value"] and time.time() < _token["expires_at"] - 3600:
        return _token["value"]
    response = requests.post(
        f"{BASE_URL}/server-authentication/oauth2/token/",
        data={"grant_type": "client_credentials", "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET},
        timeout=30,
    )
    response.raise_for_status()
    body = response.json()
    _token["value"] = body["access_token"]
    _token["expires_at"] = time.time() + body["expires_in"]
    return _token["value"]


def error_from(response):
    """Return the code and the message of an error response, whatever the shape of its body."""
    try:
        body = response.json()
    except ValueError:
        return "", response.reason
    if not isinstance(body, dict):
        return "", str(body)
    if "code" in body and "message" in body:
        return body["code"], body["message"]
    if "detail" in body:
        return "", str(body["detail"])
    if "error" in body:
        return "", str(body["error"])
    return "", "; ".join(f"{field}: {messages}" for field, messages in body.items())


def backoff(attempt):
    """Return a wait in seconds: full jitter, doubling each attempt, never more than 30."""
    return random.uniform(0, min(30, 2**attempt))


def call(method, path, retries=3, **kwargs):
    """Send a request. Wait and retry on 429. Retry a GET on 500, 502, 503 and 504."""
    for attempt in range(retries + 1):
        response = requests.request(
            method,
            f"{BASE_URL}{path}",
            headers={"Authorization": f"Bearer {get_token()}"},
            timeout=60,
            **kwargs,
        )
        if response.status_code == 429 and attempt < retries:
            time.sleep(int(response.headers.get("Retry-After", "5")))
            continue
        if response.status_code in (500, 502, 503, 504) and method == "GET" and attempt < retries:
            time.sleep(backoff(attempt))
            continue
        if response.ok:
            return response
        code, message = error_from(response)
        raise AbacumError(response.status_code, code, message, response.headers.get("X-Request-ID", ""))


try:
    call("GET", "/public-api/me")
except AbacumError as error:
    if error.status == 403:
        print("Check the token and the role of the user.")
    else:
        raise
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
const BASE_URL = "https://api.abacum.io";
const CLIENT_ID = process.env.ABACUM_CLIENT_ID;
const CLIENT_SECRET = process.env.ABACUM_CLIENT_SECRET;

const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));

class AbacumError extends Error {
  constructor(status, code, message, requestId = "") {
    super(`${status} ${code}: ${message} (request ${requestId})`);
    this.status = status;
    this.code = code;
    this.requestId = requestId;
  }
}

let token = { value: null, expiresAt: 0 };

// Return a cached token, and request a new one an hour before it expires.
async function getToken() {
  if (token.value && Date.now() < token.expiresAt - 3600 * 1000) {
    return token.value;
  }
  const response = await fetch(`${BASE_URL}/server-authentication/oauth2/token/`, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "client_credentials",
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
    }),
  });
  if (!response.ok) throw new Error(`Token request failed: ${response.status}`);
  const body = await response.json();
  token = { value: body.access_token, expiresAt: Date.now() + body.expires_in * 1000 };
  return token.value;
}

// Return the code and the message of an error response, whatever the shape of its body.
async function errorFrom(response) {
  const text = await response.text();
  if (!text) return ["", response.statusText];
  let body;
  try {
    body = JSON.parse(text);
  } catch {
    return ["", response.statusText];
  }
  if (typeof body !== "object" || body === null || Array.isArray(body)) return ["", String(body)];
  if (body.code && body.message) return [body.code, body.message];
  if (body.detail) return ["", String(body.detail)];
  if (body.error) return ["", String(body.error)];
  return ["", Object.entries(body).map(([field, messages]) => `${field}: ${messages}`).join("; ")];
}

// Return a wait in milliseconds: full jitter, doubling each attempt, never more than 30 seconds.
function backoff(attempt) {
  return Math.random() * Math.min(30, 2 ** attempt) * 1000;
}

// Send a request. Wait and retry on 429. Retry a GET on 500, 502, 503 and 504.
async function call(method, path, { retries = 3, ...options } = {}) {
  for (let attempt = 0; ; attempt++) {
    const response = await fetch(`${BASE_URL}${path}`, {
      method,
      ...options,
      headers: { Authorization: `Bearer ${await getToken()}`, ...options.headers },
    });
    if (response.status === 429 && attempt < retries) {
      await sleep((Number(response.headers.get("Retry-After")) || 5) * 1000);
      continue;
    }
    if ([500, 502, 503, 504].includes(response.status) && method === "GET" && attempt < retries) {
      await sleep(backoff(attempt));
      continue;
    }
    if (response.ok) return response;
    const [code, message] = await errorFrom(response);
    throw new AbacumError(response.status, code, message, response.headers.get("X-Request-ID") ?? "");
  }
}

try {
  await call("GET", "/public-api/me");
} catch (error) {
  if (error instanceof AbacumError && error.status === 403) {
    console.log("Check the token and the role of the user.");
  } else {
    throw error;
  }
}
```

{% endtab %}
{% endtabs %}

## What to retry

| Status                                   | Retry?                                      | Why                                                                             |
| ---------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------- |
| `429`                                    | Yes, after the `Retry-After` seconds        | The API did not process the request.                                            |
| `500`, `502`, `503` or `504` on a `GET`  | Yes, with backoff and jitter, up to 3 times | Reading again changes nothing.                                                  |
| `500`, `502`, `503` or `504` on a `POST` | No. Check the result first                  | The write may have happened. For a load, open the **Loads** tab of the dataset. |
| `400`, `403`, `404`, `422`               | No                                          | The same request fails again. Correct it, or wait for a running load to finish. |

The helper retries a write only on `429`, because the API did not process that request.

When you cannot find the cause of an error, see [Troubleshooting](/guides/troubleshooting.md).
