> ## Documentation Index
> Fetch the complete documentation index at: https://docs.proofrail.dev/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 `proofrail.exceptions`. The base class is `ProofRailPolicyError`; 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).

```
ProofRailPolicyError                # 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
└── ProofRailKillSwitchError        # 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 `ProofRailPolicyError` when you want to handle every policy-driven denial uniformly:

```python theme={null}
import proofrail
from proofrail.exceptions import ProofRailPolicyError

try:
    async with proofrail.Chain("workflow") as chain:
        await chain.record_agent_action(...)
except ProofRailPolicyError 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 an approval times out without a decision. Also raised when `fail_mode="deny"` is active and the backend is unreachable for that action class.

```python theme={null}
class ActionDeniedError(ProofRailPolicyError):
    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"`, `"local_fast_path"`, `"human_approval"`, or `"offline_stub"`.                                                                              |

### 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`                                          |
| Policy denied locally via fast-path        | `"local_fast_path"`    | Policy's `decision_reason`                                          |
| Human reviewer clicked Deny                | `"human_approval"`     | The reviewer's notes                                                |
| Approval expired without a decision        | `"human_approval"`     | `"Approval was not resolved within the configured timeout window."` |
| Backend unreachable and `fail_mode="deny"` | `"offline_stub"`       | The fail-mode reason                                                |

### Example

```python theme={null}
from proofrail.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 (backend, local fast-path, or offline stub)
        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 `ProofRailPolicyError`. In practice, most application code can catch `ProofRailPolicyError` to handle this and `ActionDeniedError` uniformly.

***

## `ProofRailKillSwitchError`

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

```python theme={null}
class ProofRailKillSwitchError(ProofRailPolicyError):
    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 proofrail.exceptions import ProofRailKillSwitchError

try:
    async with proofrail.Chain("workflow") as chain:
        await chain.record_agent_action(...)
except ProofRailKillSwitchError 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 within `backend_timeout_seconds` AND the `fail_mode` for that action class is `"deny"`. Inherits from `Exception`, not `ProofRailPolicyError` — it's an operational issue, not a policy decision.

If `fail_mode` is `"allow"` for the action class, the SDK does NOT raise — it proceeds locally and buffers the event for later replay.

### Example

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

try:
    await chain.record_agent_action(...)
except BackendUnavailableError as exc:
    logger.warning(f"ProofRail backend unreachable: {exc}")
    # The action did NOT execute. Decide whether to retry, defer, or proceed.
    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 proofrail.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)
```

***

## Handling patterns

### Catch all policy denials, distinguish by source

```python theme={null}
from proofrail.exceptions import (
    ProofRailPolicyError,
    ActionDeniedError,
    ProofRailKillSwitchError,
    BackendUnavailableError,
)

try:
    async with proofrail.Chain("workflow") as chain:
        await chain.record_agent_action(...)
except ProofRailKillSwitchError as exc:
    show_maintenance_mode(exc)
except ActionDeniedError as exc:
    handle_denial(exc)
except BackendUnavailableError as exc:
    queue_for_retry()
except ProofRailPolicyError 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(
        "proofrail.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

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

* `ProofRailKillSwitchError` 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>
