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

# Error Handling

> ModuleException, status codes, and retry patterns.

Every SDK module raises `ModuleException` when an API call fails. The exception bundles the HTTP status code with a description so you can branch on the underlying cause.

```python theme={"dark"}
from xpander_sdk import Agents
from xpander_sdk.exceptions.module_exception import ModuleException

agents = Agents()
try:
    agent = await agents.aget("non-existent-agent")
except ModuleException as e:
    print(f"[{e.status_code}] {e.description}")
```

## `ModuleException`

```python theme={"dark"}
from xpander_sdk.exceptions.module_exception import ModuleException
```

| Attribute     | Type  | Description                                                                |
| ------------- | ----- | -------------------------------------------------------------------------- |
| `status_code` | `int` | HTTP status code from the API response, or `500` for client-side failures. |
| `description` | `str` | Human-readable error description (often the raw response body).            |

The exception's string form is `[{status_code}] {description}`.

## Common status codes

| Status | Cause                                                                                    | What to do                                                                                                                               |
| ------ | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Malformed request: invalid parameters, missing required fields, schema validation.       | Inspect `description`. Fix the call site.                                                                                                |
| `401`  | Missing or invalid API key.                                                              | Verify `XPANDER_API_KEY` / `Configuration.api_key`. For self-hosted, confirm you're using the **Agent Controller** key, not a cloud key. |
| `403`  | Authenticated but not authorized: wrong organization, agent owned by another user.       | Verify `XPANDER_ORGANIZATION_ID`. Check the agent's `access_scope`.                                                                      |
| `404`  | Resource doesn't exist: bad agent/task/KB id, or the resource was deleted.               | Confirm IDs. Run `agents.list()` / `tasks.list()` to discover what exists.                                                               |
| `409`  | Conflict: duplicate creation, version mismatch on save, or worker environment collision. | Reload the resource (`task.areload()`) and retry.                                                                                        |
| `429`  | Rate limit.                                                                              | Back off exponentially. The SDK does not auto-retry on 429.                                                                              |
| `500`  | Server error, or any client-side exception (network, parsing, validation).               | Inspect `description`. For 500 specifically from the cloud, retry once or twice with backoff.                                            |

## Branching on status

```python theme={"dark"}
try:
    task = await tasks.aget(task_id="task_xyz")
except ModuleException as e:
    if e.status_code == 404:
        print("Task not found: was it deleted?")
    elif e.status_code in (401, 403):
        print("Auth issue: check credentials")
    else:
        raise
```

## Retry strategy

The SDK does **not** retry failed requests automatically (except inside `Events.start()`, which retries SSE connections internally). For idempotent operations, wrap calls in your own retry loop:

```python theme={"dark"}
import asyncio
from xpander_sdk.exceptions.module_exception import ModuleException

async def with_retry(coro_fn, *, attempts: int = 3, base_delay: float = 1.0):
    for attempt in range(1, attempts + 1):
        try:
            return await coro_fn()
        except ModuleException as e:
            if e.status_code in (429, 500, 502, 503, 504) and attempt < attempts:
                await asyncio.sleep(base_delay * (2 ** (attempt - 1)))
                continue
            raise

agent = await with_retry(lambda: agents.aget("agent-123"))
```

Avoid retrying:

* `400` / `404`: the request will keep failing.
* `acreate` / `acreate_task` without an `existing_task_id`: you may create duplicate resources. Pass `existing_task_id=...` to make creation idempotent.

## Validation errors from `Tool.ainvoke`

Tool invocations validate the payload against the tool's auto-generated Pydantic schema before sending the request. A schema mismatch raises `ValueError` (not `ModuleException`):

```python theme={"dark"}
try:
    result = await tool.ainvoke(agent_id="agent-123", payload={"bad": "shape"})
except ValueError as e:
    print(f"Payload didn't match schema: {e}")
```

Successful invocations still set `is_error=True` on the returned `ToolInvocationResult` if the remote endpoint returned an HTTP error. Check the result, not just exceptions:

```python theme={"dark"}
result = await tool.ainvoke(agent_id="agent-123", payload={"city": "NYC"})
if result.is_error:
    print(f"Tool failed [{result.status_code}]: {result.result}")
```

## Streaming errors

`task.aevents()` raises `ValueError` if the task wasn't created with `events_streaming=True`. The underlying SSE connection logs warnings on parse failures and drops malformed events; it does not raise. Network failures propagate after the SDK's internal retry budget is exhausted (see `Events`).
