> ## Documentation Index
> Fetch the complete documentation index at: https://docs.keystoneos.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Error Handling

> Error types, retry behavior, and recovery patterns.

## Error Classes

```typescript theme={null}
import { ApiError, AuthenticationError, RetryExhaustedError } from "@keystoneos/sdk";

try {
  await client.settlements.get("non-existent-id");
} catch (error) {
  if (error instanceof ApiError) {
    console.log(error.status);   // 404
    console.log(error.message);  // "Settlement not found"
    console.log(error.body);     // Raw response body
    console.log(error.headers);  // Response headers
  } else if (error instanceof AuthenticationError) {
    // M2M credentials invalid or Auth0 unavailable
    console.log(error.message);
  } else if (error instanceof RetryExhaustedError) {
    // All retries failed (429 or 5xx)
    console.log(error.lastStatus); // 503
    console.log(error.attempts);   // 3
  }
}
```

| Error                 | When                             | Properties                             |
| --------------------- | -------------------------------- | -------------------------------------- |
| `ApiError`            | Non-2xx response                 | `status`, `message`, `body`, `headers` |
| `AuthenticationError` | Token fetch/refresh failed       | `message`                              |
| `RetryExhaustedError` | All retries exhausted on 429/5xx | `lastStatus`, `attempts`               |

### Common status codes

| Status | Meaning          | Typical cause                                            |
| ------ | ---------------- | -------------------------------------------------------- |
| 400    | Bad request      | Malformed JSON or invalid field format                   |
| 401    | Unauthorized     | Invalid or expired M2M token                             |
| 403    | Forbidden        | Missing required scope or suspended platform             |
| 404    | Not found        | Resource doesn't exist or belongs to another environment |
| 409    | Conflict         | State conflict (e.g., settlement already finalized)      |
| 422    | Validation error | Business rule violation (e.g., timeout in the past)      |
| 429    | Rate limited     | Too many requests (auto-retried)                         |
| 5xx    | Server error     | KeyStone API issue (auto-retried)                        |

***

## Retry Behavior

The SDK automatically retries on 429 (rate limit) and 5xx (server error):

| Setting          | Default     | Description                                  |
| ---------------- | ----------- | -------------------------------------------- |
| `maxRetries`     | `3`         | Max retry attempts                           |
| `retryBaseDelay` | `500`       | Base delay in ms                             |
| Strategy         | Exponential | `baseDelay * 2^attempt * jitter(0.75, 1.25)` |
| 429 handling     | Retry-After | Respects the `Retry-After` header if present |

**Non-retryable errors** (4xx except 429) fail immediately. There is no point retrying a 404 or 422.

### Example timing

With default settings (500ms base, 3 retries):

* Attempt 1: immediate
* Attempt 2: \~500ms delay
* Attempt 3: \~1000ms delay
* Attempt 4: \~2000ms delay
* After attempt 4: throws `RetryExhaustedError`

***

## Recovery Patterns

### Idempotent retries

For creation endpoints, always use an idempotency key so retries are safe:

```typescript theme={null}
const key = client.generateIdempotencyKey();

// If this fails mid-request (network error), retrying with the same key
// returns the existing resource instead of creating a duplicate
const settlement = await client.settlements.create({
  idempotencyKey: key,
  // ...
});
```

### Handling rate limits

The SDK handles 429 automatically. If you need custom logic:

```typescript theme={null}
try {
  await client.settlements.list();
} catch (error) {
  if (error instanceof RetryExhaustedError && error.lastStatus === 429) {
    // All retries exhausted on rate limit
    // Wait longer and retry, or queue for later
  }
}
```

### Handling state conflicts

Settlement operations may return 409 when the settlement has moved to a different state:

```typescript theme={null}
try {
  await client.settlements.submitComplianceDecision(id, { decision: "approve" });
} catch (error) {
  if (error instanceof ApiError && error.status === 409) {
    // Settlement is no longer in COMPLIANCE_CHECKING state
    // Fetch current state and act accordingly
    const settlement = await client.settlements.get(id);
    console.log(`Current state: ${settlement.state}`);
  }
}
```
