# Error handling

> Run and stream errors, retry behaviour, and partial results after a failure.

Source: https://maincode.com/docs/python-agent-sdk-error-handling
Section: Agent SDK · Matilda documentation

---

## `MatildaAgentRunError`

Raised on HTTP-level failures (non-2xx response from the server).

```python
class MatildaAgentRunError(MatildaError):
    status: int         # HTTP status code
    response_text: str  # Raw response body
```

## `MatildaAgentStreamError`

Raised by `run()` when the stream emits an error event (unless `throw_on_stream_error=False`). Carries the full `AgentRunResult` with whatever was collected before the error.

```python
class MatildaAgentStreamError(MatildaError):
    code: str                    # ChatErrorCode of the first error ('unknown' when empty)
    errors: list[StreamErrorDetail]
    result: AgentRunResult
```

```python
from matilda_agent_sdk import MatildaAgentRunError, MatildaAgentStreamError

try:
    await runner.run(agent, prompt)
except MatildaAgentStreamError as err:
    print("Stream error code:", err.code)
    print("Partial output before error:", err.result.final_output)
    print("All errors:", err.errors)
except MatildaAgentRunError as err:
    print("HTTP error status:", err.status)
    print("Response body:", err.response_text)
```

## `SafetyReplaceError`

Raised by `stream_text()`, `run_text()`, `stream_object()`, and `run_object()` when the server replaces the output via a safety filter. `str(err)` contains the replacement text (or empty string), and `categories` lists the safety categories.

```python
class SafetyReplaceError(MatildaError):
    categories: list[str]
```

## `MatildaObjectParseError`

Raised by `stream_object()` / `run_object()` when the response cannot be parsed as JSON or fails pydantic validation — e.g. a truncated stream (see [Structured output](https://maincode.com/docs/python-agent-sdk-structured-output)). `raw` holds the full response text; `cause` is the underlying `json.loads` or pydantic error. Safety replacement does not surface here — it raises `SafetyReplaceError` first.

```python
class MatildaObjectParseError(MatildaError):
    raw: str
    cause: BaseException | str
```

## Client SDK errors (re-exported)

The client SDK's error types are re-exported unchanged: `MatildaError` (shared base), `MatildaAPIError` (`status`, `response_text`), `QuotaExceededError` (structured 429 quota attributes), `MatildaStreamError` (`code`), and `UploadError`. `AuthError` (`code`, `retryable`) is importable from `matilda_client`.

## Chat error codes (`ChatErrorCode`)

These codes are emitted via the `ResponseError` stream event and appear in `AgentRunResult.errors` (mirrored on `StreamErrorDetail.code`):

| Code | Description |
| - | - |
| `internal_error` | Server-side failure. |
| `upstream_unavailable` | The AI model is not responding. |
| `rate_limited` | Too many requests. |
| `content_blocked` | Safety filter blocked the content. |
| `stream_aborted` | The stream was interrupted before completion. |
| `deadline_exceeded` | The response did not finish before the deadline. |
| `context_too_large` | The conversation is too long for the model. |
| `url_read_incomplete` | A URL the server tried to read did not finish loading. |
| `request_budget_exceeded` | The request exceeded its server-side budget. |
| `stalled` | No SSE events for the configured stall window. |
| `unknown` | Unclassified error (old server without typed codes). |

## Stall watchdog

The streaming parser arms an idle-event watchdog. If no SSE event arrives for `stall_timeout_ms` milliseconds, the stream is considered dead and aborted with a `'stalled'` error.

- **Default:** `45_000` ms (`DEFAULT_AGENT_STALL_TIMEOUT_MS`)
- **Disable:** Pass `stall_timeout_ms=0` (not recommended)

## Retry behaviour

The SDK retries retryable errors within a single turn. Retries are controlled by `max_retries` (default: `0` — no retries; `DEFAULT_MAX_RETRIES`).

**Retryable errors:**

- HTTP 429 (rate limited), 408 (request timeout), 5xx (server errors)
- Network errors: `httpx.ConnectError`, `httpx.ConnectTimeout`, `httpx.ReadTimeout`, `httpx.WriteTimeout`, `httpx.PoolTimeout`, `httpx.RemoteProtocolError`
- SSE stall watchdog (`stalled` error code)
- `TimeoutError`

**Retry delay:** Exponential backoff with jitter (1 s base), capped at 30 seconds.

**Important:** Retries only happen when no text has been yielded to the consumer yet. Retrying past that point would replay text the caller already has.

When a retry occurs, a `TurnRetrying` event is emitted:

```python
async for event in runner.stream(agent, prompt, max_retries=3):
    if event.type == "turn.retrying":
        print(f"Retry {event.attempt}/{event.max_retries} in {event.delay_ms}ms: {event.error['message']}")
```

## Error handling example

```python title="error_handling.py"
import asyncio

from matilda_agent_sdk import (
    MatildaAgentRunError,
    MatildaAgentStreamError,
    Runner,
    SafetyReplaceError,
)


async def main():
    runner = Runner()

    try:
        text = await runner.run_text(agent, "Hello!")
        print(text)
    except MatildaAgentStreamError as err:
        print(f"Stream error: {err.code} — {err}")
        print("Partial output:", err.result.final_output)
    except MatildaAgentRunError as err:
        if err.status == 401:
            print("Session expired — re-authenticate.")
        elif err.status == 429:
            print("Rate limited — slow down.")
        else:
            print(f"API error {err.status}: {err.response_text}")
    except SafetyReplaceError as err:
        print(f"Safety filter: {', '.join(err.categories)}")


asyncio.run(main())
```
