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

# Error Handling

> Handle PikarcBlockedError and understand all deny reason codes.

## PikarcBlockedError

When Pikarc denies a run start, model call, or tool call, the SDK raises `PikarcBlockedError`. This happens **before** your function executes — no tokens are consumed, no tools are invoked.

```python theme={null}
from pikarc import AsyncPikarc, PikarcBlockedError

guard = AsyncPikarc(api_key="lg_...", base_url="http://localhost:8000")

try:
    async with guard.run(user_id="alice") as run:
        response = await run.model_call(
            fn=lambda: openai.chat.completions.create(...),
            model="gpt-4o",
        )
except PikarcBlockedError as e:
    print(e.reason)       # "WORKSPACE_DAILY_BUDGET_EXCEEDED"
    print(e.deny_reason)  # "WORKSPACE_DAILY_BUDGET_EXCEEDED"
finally:
    await guard.close()
```

### Attributes

| Attribute     | Type  | Description                                         |
| ------------- | ----- | --------------------------------------------------- |
| `reason`      | `str` | Human-readable reason for the denial                |
| `deny_reason` | `str` | Machine-readable deny reason code (see table below) |

## Deny Reason Codes

| Code                              | Description                                                           | When Checked    |
| --------------------------------- | --------------------------------------------------------------------- | --------------- |
| `KILL_SWITCH_ACTIVE`              | The workspace kill switch is enabled. All runs and steps are blocked. | Run start, Step |
| `USER_BLOCKED`                    | This SDK user has been individually blocked.                          | Run start, Step |
| `WORKSPACE_DAILY_BUDGET_EXCEEDED` | Total workspace spend today exceeds the configured daily budget.      | Run start, Step |
| `USER_DAILY_BUDGET_EXCEEDED`      | This user's spend today exceeds the configured per-user daily budget. | Run start, Step |
| `MONTHLY_RUN_LIMIT_EXCEEDED`      | The workspace has reached its monthly run limit (based on plan tier). | Run start       |
| `MAX_CONCURRENT_RUNS_EXCEEDED`    | Too many runs are active simultaneously.                              | Run start       |
| `RUN_ALREADY_ENDED`               | The run has already been ended (COMPLETED or FAILED).                 | Step            |

## Where Denials Can Happen

### At run start

When `guard.run()` is entered, Pikarc evaluates all 6 rules. If denied, the `async with` block is never entered:

```python theme={null}
try:
    async with guard.run(user_id="alice") as run:
        # This code never executes if run start is denied
        ...
except PikarcBlockedError as e:
    # Handle run-start denial
    print(f"Run blocked: {e.deny_reason}")
```

### At step creation

When `run.model_call()` or `run.tool_call()` is called, rules 1-4 are re-evaluated. If denied, `fn` is never called:

```python theme={null}
async with guard.run(user_id="alice") as run:
    try:
        response = await run.model_call(
            fn=lambda: expensive_model_call(),
            model="gpt-4o",
        )
    except PikarcBlockedError as e:
        # The model was never called — no tokens consumed
        print(f"Step blocked: {e.deny_reason}")
```

## HTTP Errors

The SDK uses `httpx` under the hood. Network errors or non-2xx responses from the Pikarc API will raise `httpx.HTTPStatusError`:

```python theme={null}
import httpx

try:
    async with guard.run(user_id="alice") as run:
        ...
except PikarcBlockedError:
    # Guardrail denial
    ...
except httpx.HTTPStatusError as e:
    # API error (401 unauthorized, 500 server error, etc.)
    print(f"API error: {e.response.status_code}")
```

## Best Practices

1. **Always catch `PikarcBlockedError`** — it's expected behavior, not a bug. Return a user-friendly message.
2. **Always call `guard.close()`** — use a `finally` block or async context manager to clean up the HTTP client.
3. **Catch at the right level** — catch around `guard.run()` for run-start denials, or around individual `model_call()`/`tool_call()` for step-level denials.
4. **Log the deny reason** — `e.deny_reason` tells you exactly which guardrail triggered. Use it for monitoring and alerting.
