# Error handling

> Typed error classes, chat error codes, and the SSE stall watchdog.

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

---

Every error raised by this SDK derives from `MatildaError`.

## `MatildaAPIError`

Raised on non-2xx HTTP responses from the API.

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

## `QuotaExceededError`

A `MatildaAPIError` subclass for HTTP 429 with a structured `quota_exceeded` body — the daily quota is spent. Carries the quota payload as attributes:

```python
class QuotaExceededError(MatildaAPIError):
    tier: str | None
    messages_used: int | None
    messages_limit: int | None
    tokens_used: int | None
    tokens_limit: int | None
    resets_at: str | None
    upgrade_action: str | None
```

## `MatildaStreamError`

A terminal `error` event on the chat SSE stream, surfaced by the text/object helpers. `str(err)` is `f"{code}: {message}"`.

```python
class MatildaStreamError(MatildaError):
    code: str  # a ChatErrorCode
```

## `SafetyReplaceError`

Raised by `chat.stream_text()`, `chat.stream_object()`, and `chat.create_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 that triggered the replacement.

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

## `MatildaObjectParseError`

Raised by `chat.stream_object()` and `chat.create_object()` when the response cannot be parsed as JSON or fails pydantic validation — e.g. when a stream is truncated. `raw` contains the full response text; `cause` is the underlying `json.loads` or pydantic error. See [Structured output](https://maincode.com/docs/python-client-sdk-structured-output).

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

## `UploadError`

Raised when a file upload fails after retries (also used for missing-token upload attempts).

## `AuthError`

Raised by auth flows. The `code` field is an OAuth error code (e.g. `'invalid_grant'`, `'authorization_pending'`, `'expired_token'`, `'access_denied'`). The `retryable` field distinguishes transient failures (5xx, network) from permanent ones (revoked refresh token).

```python
class AuthError(MatildaError):
    code: str
    retryable: bool
```

## Chat error codes (`ChatErrorCode`)

These codes are emitted via the `ResponseError` stream event and appear in `MatildaChatResponse.errors`. Also available as the `CHAT_ERROR_CODES` frozenset:

| 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_STALL_TIMEOUT_MS` — 3× the server's 15-second keep-alive ping cadence)
- **Disable:** Pass `stall_timeout_ms=0` (not recommended — mobile loses background-to-foreground hung-stream recovery)

## Error handling example

```python title="errors.py"
from matilda_client import (
    MatildaAPIError,
    MatildaClient,
    QuotaExceededError,
    SafetyReplaceError,
)


async def main():
    async with MatildaClient(token="...") as client:
        try:
            text = await client.chat.create_text(input="Hello!")
            print(text)
        except QuotaExceededError as err:
            print(f"Daily quota exceeded — resets at {err.resets_at}.")
        except MatildaAPIError 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)}")
```
