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

# Error Handling

> Production-grade patterns for handling Arcus API errors, retries, and network failures.

## The golden rule

**Never crash on an API error.** Every integration should gracefully degrade when Arcus is unavailable, rate-limited, or returns a validation error.

## Error classification

Before writing retry logic, classify errors by whether they are safe to retry:

| Class            | Examples                            | Retry?                           |
| ---------------- | ----------------------------------- | -------------------------------- |
| **Transient**    | 500, 502, 503, 504, network timeout | Yes, with backoff                |
| **Rate limited** | 429                                 | Yes, after `Retry-After` seconds |
| **Client error** | 400, 401, 403, 404, 409, 422        | No -- fix the request first      |

Retrying a 422 Unprocessable Entity is wasted effort; the request will always fail until you fix the payload.

## Exponential backoff

For transient errors and rate limits, use exponential backoff with jitter:

```javascript theme={null}
async function withRetry(fn, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (err) {
      const isRetryable = err.status >= 500 || err.status === 429;
      if (!isRetryable || attempt === maxRetries) throw err;

      const retryAfter = err.status === 429
        ? parseInt(err.headers?.['retry-after'] ?? '1', 10) * 1000
        : Math.pow(2, attempt) * 1000 + Math.random() * 1000; // jitter

      await new Promise((resolve) => setTimeout(resolve, retryAfter));
    }
  }
}
```

## Idempotency on retry

Always use `Idempotency-Key` when retrying POST/PATCH/DELETE requests. Generate the key before the first attempt and reuse the same key on every retry:

```javascript theme={null}
const idempotencyKey = crypto.randomUUID();

const order = await withRetry(() =>
  arcus.orders.create(params, { idempotencyKey })
);
```

If Arcus received and processed the original request but your network timed out, the retry returns the original response without creating a duplicate.

## Handling specific errors

### 401 Unauthorized

Your API key is missing, invalid, or expired. Check:

* `Authorization` header is present and starts with `Bearer `
* Key has not been deleted in Settings > Developers
* Key is for the correct mode (test vs live) for the environment you are calling

### 403 insufficient\_scope

Your key lacks a required scope. The error includes `required: <scope>`:

```json theme={null}
{ "error": "insufficient_scope", "required": "orders:write" }
```

Add the scope to your key in Settings > Developers, or create a new key with the correct scopes.

### 404 not\_found

The resource does not exist or belongs to a different entity. Common causes:

* Typo in the ID
* Using a test-mode ID with a live-mode key
* The record was deleted

### 409 conflict

State conflict. Common causes:

* `idempotency_key_mismatch`: same key reused with a different body -- generate a new key
* `resource_in_use`: cannot delete a resource that is referenced elsewhere -- see the `references` field in the error body for what is blocking the delete

### 422 Unprocessable Entity

Validation failure. The `param` field identifies which field failed and `hint` explains why:

```json theme={null}
{
  "error": "invalid_field_value",
  "param": "line_items[0].quantity",
  "hint": "Quantity must be greater than 0."
}
```

## Structured error logging

Log enough context to debug without logging sensitive data:

```javascript theme={null}
try {
  const order = await arcus.orders.create(params);
} catch (err) {
  if (err instanceof ArcusAPIError) {
    logger.error('Arcus API error', {
      code: err.code,           // machine-readable; safe to log
      hint: err.hint,           // human-readable; safe to log
      status: err.status,       // HTTP status
      requestId: err.requestId, // include in support tickets
      param: err.param,         // which field failed (422 only)
      // DO NOT log: apiKey, payload body (may contain PII)
    });
  }
}
```

## Circuit breaker

For high-volume integrations, add a circuit breaker to avoid hammering a degraded API:

```javascript theme={null}
import CircuitBreaker from 'opossum';

const breaker = new CircuitBreaker(
  (params) => arcus.orders.create(params),
  {
    timeout: 5000,          // trip if request takes >5s
    errorThresholdPercentage: 50, // trip if >50% of requests fail
    resetTimeout: 30000,    // try again after 30s
  },
);

breaker.fallback(() => ({ error: 'service_unavailable', queued: true }));
```

## Monitoring

Set up alerts on:

* `5xx` error rate spike (Arcus outage)
* `429` rate rising (approaching rate limit; add backpressure or request a limit increase)
* `401`/`403` errors (key expiry or accidental deletion)

Include `request_id` in your alert payloads so on-call engineers can share it with Arcus support.
