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

# Errors

> Typed exceptions the SDK throws, and how to recover from each.

The Tally SDK turns every non-2xx response into a typed exception you can catch and branch on. The shape is consistent across the surface: every error is an instance of `TallyError` (or a subclass), and carries `type`, `message`, `code?`, `status`, and `details?` fields.

## Class hierarchy

```
TallyError
├── AuthenticationError    // 401, 403
├── NotFoundError          // 404
├── ValidationError        // 422 (validation_failed)
├── RateLimitError         // 429
└── ConflictError          // 409
```

`TallyError` is the catch-all base. Anything the server returns that the SDK doesn't have a specific subclass for surfaces as a plain `TallyError`.

## Error shape

Every error carries these fields:

| Field     | Type                  | Description                                                                                     |
| --------- | --------------------- | ----------------------------------------------------------------------------------------------- |
| `type`    | `string`              | The server's structured error type — `unauthenticated`, `validation_failed`, etc.               |
| `message` | `string`              | Human-readable description. Safe to log; doesn't include user data.                             |
| `code`    | `string \| undefined` | Specific failure mode within `type`. See [Error codes](/sdk/payments#error-codes) for payments. |
| `status`  | `number`              | HTTP status code.                                                                               |
| `details` | `unknown`             | Server-provided structured context (often the failed Zod issues).                               |

## The classes

### `AuthenticationError` — 401 / 403

```ts theme={null}
class AuthenticationError extends TallyError {}
// type: "unauthenticated" or "forbidden"
```

Fires when the API key is missing, malformed, revoked, or out of its rotation grace window. Also fires when the key authorizes a different account or mode than the resource you're acting on.

**Recovery:** rotate the key. If the key was recently rotated and is still in its 24h grace window, the new key should already be working — the error indicates the *old* key is past the grace cutoff. Don't retry with the same key.

### `NotFoundError` — 404

```ts theme={null}
class NotFoundError extends TallyError {}
// type: "not_found"
```

The resource you asked for doesn't exist in this (account, mode). Often a sign of a mode mismatch — a `tly_test_` key looking for a live-mode agent gets a `NotFoundError`, not a `forbidden`, because the resource is genuinely invisible to the key's scope.

**Recovery:** check the id, check the mode, log it, and surface "not found" in your UI as appropriate.

### `ValidationError` — 400

```ts theme={null}
class ValidationError extends TallyError {}
// type: "validation_failed"
```

The request payload failed server-side validation. `err.details` typically contains the structured Zod issue list. `err.code` carries a specific failure mode where applicable (`amount_invalid`, `address_invalid`).

```ts theme={null}
try {
  await tally.payments.create({ ... });
} catch (err) {
  if (err instanceof ValidationError) {
    console.error("validation failed:", err.message, err.details);
  }
  throw err;
}
```

**Recovery:** fix the input. Don't retry the same payload.

<Note>
  Most policy-related errors on `payments.create()` (per-tx max exceeded, daily cap exceeded, recipient not allowed, etc.) are returned as `403 forbidden` from the server, which the SDK maps to `AuthenticationError` — not `ValidationError`. The naming is awkward; see [Payments — Error codes](/sdk/payments#error-codes) for the full mapping.
</Note>

### `RateLimitError` — 429

```ts theme={null}
class RateLimitError extends TallyError {}
// type: "rate_limited"
```

You're sending requests faster than the account's per-second ceiling. The response includes a `Retry-After` hint (the SDK doesn't surface this on the error object today; check `err.details` if present).

**Recovery:** exponential backoff. The pattern:

```ts theme={null}
for (let attempt = 0; attempt < 5; attempt++) {
  try {
    return await tally.payments.create({ ... });
  } catch (err) {
    if (err instanceof RateLimitError) {
      await sleep(1000 * 2 ** attempt); // 1s, 2s, 4s, 8s, 16s
      continue;
    }
    throw err;
  }
}
```

Combine with an `idempotency_key` so retries are safe — see [Payments](/sdk/payments).

### `ConflictError` — 409

```ts theme={null}
class ConflictError extends TallyError {}
// type: "conflict"
```

The request collides with existing state. The class is exported for completeness — the public v1 surface doesn't throw it today, but it may surface from future endpoints (e.g. `agents.delete()` on an agent with active grants).

**Recovery:** don't retry without changing the request.

### `TallyError` (base)

```ts theme={null}
class TallyError extends Error {
  readonly type: string;
  readonly code?: string;
  readonly status: number;
  readonly details?: unknown;
}
```

Anything the SDK can't map to a more specific class falls through as a `TallyError`. The most common cases are 5xx server errors (rare) and network failures bubbled up through `fetch`.

**Recovery:** depends on `status`. 5xx should be retried with exponential backoff. Network errors (`status: 0`) should be retried with idempotency keys.

## Idiomatic catch

```ts theme={null}
import {
  AuthenticationError,
  ValidationError,
  RateLimitError,
  ConflictError,
  NotFoundError,
  TallyError,
} from "@tallyforagents/sdk";

try {
  await tally.payments.create({ ... });
} catch (err) {
  if (err instanceof AuthenticationError) {
    // Rotate the key, alert ops.
    return errorPage("auth_failed");
  }
  if (err instanceof AuthenticationError && err.code === "amount_too_large") {
    // Policy-layer violation; prompt to top up the permission.
    return promptForPermissionTopUp();
  }
  if (err instanceof RateLimitError) {
    return backoffAndRetry();
  }
  // Anything else → log and surface as a generic failure.
  console.error("unexpected tally error:", err);
  throw err;
}
```

Branch on the class first; branch on `err.code` second; let everything else propagate. Catching `TallyError` to silently swallow is almost always a bug.

## Server error shape

If you're curious what the SDK is parsing, every error response from Tally looks like this:

```json theme={null}
{
  "error": {
    "type": "validation_failed",
    "message": "Amount exceeds per-tx max.",
    "code": "insufficient_allowance",
    "details": { "max_per_tx_usdc": "10", "requested": "15" }
  }
}
```

The SDK's `makeError(payload, status)` maps `type` → class — `unauthenticated` and `forbidden` both become `AuthenticationError`, `not_found` becomes `NotFoundError`, etc.

That mapping is in `packages/sdk/src/errors.ts` if you want to see the exact switch.
