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

# Exceptions

> Every exception type the SDK raises, with attributes and handling patterns

# Exceptions

All exceptions live in `arclasp.exceptions`. The base class is `ArclaspPolicyError`; catching it catches every policy-related exception the SDK raises. A few exceptions extend `Exception` directly when they're not policy decisions (timeouts, backend connectivity, auto-pause).

```
ArclaspPolicyError                # base for policy-driven exceptions
├── ActionDeniedError               # policy denied an action or human reviewer denied it
├── PolicyViolationError            # a policy condition was violated outside the normal evaluation path
└── ArclaspKillSwitchError        # org-wide kill switch denied the action

Exception
├── BackendUnavailableError         # the backend is unreachable and fail-mode is deny
├── ChainTimeoutError               # the chain ran past its configured timeout
└── ChainAutoPausedError            # an auto-pause rule (event count, duration, token budget) fired
```

Catch `ArclaspPolicyError` when you want to handle every policy-driven denial uniformly:

```python theme={null}
import arclasp
from arclasp.exceptions import ArclaspPolicyError

try:
    async with arclasp.Chain("workflow") as chain:
        await chain.record_agent_action(...)
except ArclaspPolicyError as exc:
    logger.error(f"Policy denied the action: {exc}")
    fallback_workflow()
```

For operational issues (backend down, chain timed out, runaway behavior auto-paused), catch the relevant `Exception` subclass instead.

***

## `ActionDeniedError`

Raised when a policy denies an action, when a human reviewer denies an approval, or when the backend marks a pending approval as timed out. Backend unavailability is a separate case — see `BackendUnavailableError` below.

```python theme={null}
class ActionDeniedError(ArclaspPolicyError):
    message: str
    policy_name: str | None
    condition: str | None
    chain_context: dict | None
    remediation: str | None
    docs_url: str | None
    decision_source: str | None
```

### Attributes

| Attribute         | Description                                                                                                                                                                                                            |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `message`         | Human-readable summary of why the action was denied.                                                                                                                                                                   |
| `policy_name`     | Which policy made the decision (default policy name or custom policy name).                                                                                                                                            |
| `condition`       | What about the action triggered the denial — the policy's `decision_reason`, the approver's notes, or a timeout message like `"Approval was not resolved within the configured timeout window."`                       |
| `chain_context`   | Dict containing chain-level context. Includes `chain_id` and other state at the point of denial.                                                                                                                       |
| `remediation`     | Suggested fix (e.g., `"update cumulative_financial_threshold_usd in init() or approve via dashboard"`).                                                                                                                |
| `docs_url`        | Link to the relevant docs page for this kind of denial.                                                                                                                                                                |
| `decision_source` | Where the decision came from: `"backend_evaluation"` for a direct policy decision, or `"human_approval"` once a `require_approval` gate has been resolved by a reviewer (approved, denied, or backend-side timed out). |

### Distinguishing denial reasons

`ActionDeniedError` doesn't have a single `reason` enum. To tell what kind of denial occurred, check `decision_source` and `condition` together:

| Scenario                                                | `decision_source`      | `condition`                                                 |
| ------------------------------------------------------- | ---------------------- | ----------------------------------------------------------- |
| Policy denied at the backend                            | `"backend_evaluation"` | Policy's `decision_reason`                                  |
| Human reviewer clicked Deny                             | `"human_approval"`     | The reviewer's notes                                        |
| Approval expired on the backend side without a decision | `"human_approval"`     | Timeout condition text                                      |
| The SDK's own local polling window expired first        | —                      | Raises `ChainTimeoutError` instead, not `ActionDeniedError` |

### Example

```python theme={null}
from arclasp.exceptions import ActionDeniedError

try:
    await chain.record_agent_action(
        agent_name="commitment-agent",
        action_type="tool_call",
        action_name="record_purchase",
        payload={"amount_usd": 75000},
    )
except ActionDeniedError as exc:
    print(f"Policy: {exc.policy_name}")
    print(f"Condition: {exc.condition}")
    print(f"What to do: {exc.remediation}")
    print(f"Docs: {exc.docs_url}")
    print(f"Chain context: {exc.chain_context}")

    if exc.decision_source == "human_approval":
        if exc.condition and "not resolved within" in exc.condition:
            # No reviewer responded — retry tomorrow
            schedule_retry()
        else:
            # Reviewer explicitly denied with notes in `condition`
            chain_id = (exc.chain_context or {}).get("chain_id")
            escalate_to_manual_review(chain_id)
    else:
        # Policy denial at the backend
        log_policy_denial(exc)
```

### Don't suppress and continue

Suppressing `ActionDeniedError` and retrying the same action accomplishes nothing — the policy will deny it again. If you catch it, either:

1. Change the situation (lower amount, different recipient, different agent) and retry
2. Escalate the work to a human
3. Log the denial and continue with the next part of your workflow

The `message`, `remediation`, and `docs_url` fields are built to be shown to your application's user — they tell a human reading logs what to do.

***

## `PolicyViolationError`

Raised when a policy condition is violated outside the normal action-decision path — for example, when chain-level cumulative limits are checked at chain start or during background reconciliation rather than at an individual action evaluation.

Inherits from `ArclaspPolicyError`. In practice, most application code can catch `ArclaspPolicyError` to handle this and `ActionDeniedError` uniformly.

***

## `ArclaspKillSwitchError`

Raised when the organization's kill switch is active and an action is attempted.

```python theme={null}
class ArclaspKillSwitchError(ArclaspPolicyError):
    message: str
    organization_id: str | None
    reason: str | None
```

### When raised

Whenever the kill switch is on for your org and `record_agent_action` is called. This is distinct from a regular `ActionDeniedError` so applications can show a different message (maintenance mode vs. policy violation).

### Attributes

| Attribute         | Description                                                                    |
| ----------------- | ------------------------------------------------------------------------------ |
| `message`         | Default: `"All agent actions are denied: organisation kill switch is active"`. |
| `organization_id` | The org the kill switch applies to.                                            |
| `reason`          | The reason the admin gave when activating the kill switch.                     |

### Example

```python theme={null}
from arclasp.exceptions import ArclaspKillSwitchError

try:
    async with arclasp.Chain("workflow") as chain:
        await chain.record_agent_action(...)
except ArclaspKillSwitchError as exc:
    return {
        "status": "maintenance",
        "message": "Agent activity is temporarily paused.",
        "reason": exc.reason,
    }
```

See [Kill switch](/guides/kill-switch) for activation and resumption.

***

## `BackendUnavailableError`

Raised when the SDK can't reach the backend after retries (`max_retries`, `backend_timeout_seconds`) or the backend keeps returning 5xx/429. Inherits from `Exception`, not `ArclaspPolicyError` — it's an operational issue, not a policy decision.

This is always raised on backend unavailability, regardless of `fail_mode` / `fail_modes`. `fail_mode="allow"` is accepted for call-signature compatibility but is deprecated and has no effect — it emits a `DeprecationWarning` and the SDK still fails closed. There is no local buffering or "proceed and sync later" behavior in governed execution; the action simply did not run.

### Example

```python theme={null}
from arclasp.exceptions import BackendUnavailableError

try:
    await chain.record_agent_action(...)
except BackendUnavailableError as exc:
    logger.warning(f"Arclasp backend unreachable: {exc}")
    # The action did NOT execute. Decide whether to retry, defer, or fail the workflow.
    queue_for_retry()
```

***

## `ChainTimeoutError`

Raised when a chain exceeds its configured runtime without completing. Inherits from `Exception`.

Long-running batch workflows should configure a longer timeout via SDK config. The exception is meant to catch chains that have hung — not chains that are doing legitimate long work.

***

## `ChainAutoPausedError`

Raised when a chain hits one of the auto-pause limits — event count, chain duration, or token budget. Inherits from `Exception`.

See [Policies](/concepts/policies) for the default auto-pause thresholds.

### Example

```python theme={null}
from arclasp.exceptions import ChainAutoPausedError

try:
    await chain.record_agent_action(...)
except ChainAutoPausedError as exc:
    # The chain has stopped to prevent runaway behavior
    logger.error(f"Chain auto-paused: {exc}")
    investigate_chain(chain.id)
```

***

## Transport and setup errors

Not every failure is a policy decision. Two plain-`Exception` cases come from lower in the stack:

* **`RuntimeError`** — raised by the SDK itself for setup mistakes: calling any `Chain` method before `arclasp.init()`, or before entering the chain as a context manager (`"Chain has not been started. Use it as a context manager."`), or using synchronous `with Chain(...)` from inside a running async event loop.
* **`httpx.HTTPStatusError`** — raised directly (not wrapped) on 4xx responses, since those are deterministic client errors that retrying won't fix. The two you'll see most: `401` (missing or invalid API key) and `403` (the key's role doesn't have permission for that operation).

## Troubleshooting

**Hosted example accidentally hitting localhost.** `backend_url` defaults to `http://localhost:8000`. If you copied an example without adding `backend_url="https://api.proofrail.dev"`, every call will fail to connect — you'll see a connection error surface as `BackendUnavailableError` after retries are exhausted, not a clean "wrong URL" message. Check your `arclasp.init()` call first.

**401 — missing or invalid API key.** `httpx.HTTPStatusError` with `status_code=401`. Confirm `ARCLASP_API_KEY` is set and that the key hasn't been revoked from the dashboard.

**403 — role/permission problem.** `httpx.HTTPStatusError` with `status_code=403`. The authenticated key or user doesn't have the role required for that endpoint (e.g., approving requires the `approver` role).

**Approval still pending / terminal waiting.** This is expected while `record_agent_action()` blocks on a `require_approval` decision — it polls every 5 seconds. Open the dashboard's `/dashboard/approvals` and resolve the pending item; the terminal resumes on the next poll after you do.

**Approval denied.** `record_agent_action()` raises `ActionDeniedError` with `decision_source="human_approval"` and the reviewer's notes in `condition`.

**Approval timed out.** Either `ActionDeniedError` (the backend marked the approval as timed out) or `ChainTimeoutError` (the SDK's own local polling window, `default_approval_timeout_hours`, expired first). See [Distinguishing denial reasons](#distinguishing-denial-reasons) above.

## Handling patterns

### Catch all policy denials, distinguish by source

```python theme={null}
from arclasp.exceptions import (
    ArclaspPolicyError,
    ActionDeniedError,
    ArclaspKillSwitchError,
    BackendUnavailableError,
)

try:
    async with arclasp.Chain("workflow") as chain:
        await chain.record_agent_action(...)
except ArclaspKillSwitchError as exc:
    show_maintenance_mode(exc)
except ActionDeniedError as exc:
    handle_denial(exc)
except BackendUnavailableError as exc:
    queue_for_retry()
except ArclaspPolicyError as exc:
    # Catch-all for any other policy issues
    log_and_fail_gracefully(exc)
```

### Distinguish human denial from timeout

```python theme={null}
try:
    await chain.record_agent_action(...)
except ActionDeniedError as exc:
    if exc.decision_source == "human_approval":
        if exc.condition and "not resolved within" in exc.condition:
            # No reviewer responded — retry tomorrow
            queue_for_retry(chain.id)
        else:
            # Reviewer explicitly denied
            notify_workflow_owner(exc)
    else:
        # Policy denial (not human)
        log_policy_block(exc)
```

### Log denials for analytics

```python theme={null}
try:
    await chain.record_agent_action(...)
except ActionDeniedError as exc:
    metrics.increment(
        "arclasp.denial",
        tags={
            "policy": exc.policy_name or "unknown",
            "source": exc.decision_source or "unknown",
        },
    )
    raise  # re-raise after logging
```

Tracking which policies deny most often is a strong signal for what to revisit — either the policy is wrong, the workflow is wrong, or the thresholds need adjusting.

### Don't catch what you can't fix

`ArclaspKillSwitchError` and `ChainAutoPausedError` shouldn't be caught and worked around in normal application code:

* `ArclaspKillSwitchError` is an admin saying "stop everything". Honor it; don't bypass.
* `ChainAutoPausedError` means runaway behavior was detected. Stop and investigate; don't retry blindly.

`ActionDeniedError` is the exception you'd routinely catch in application logic. `BackendUnavailableError` is the one to catch when you need explicit retry or queue handling.

## Where to go next

<CardGroup cols={2}>
  <Card title="SDK API" icon="code" href="/reference/sdk-api">
    Type signatures for everything else.
  </Card>

  <Card title="Configuration" icon="gear" href="/reference/configuration">
    The init() options that drive these errors.
  </Card>

  <Card title="Human approval" icon="user-check" href="/guides/human-approval">
    Where approval-related denials come from.
  </Card>

  <Card title="Kill switch" icon="power-off" href="/guides/kill-switch">
    Activating, resuming, and handling in code.
  </Card>
</CardGroup>
