> ## Documentation index
> Fetch the complete documentation index at: https://upstash.com/docs/llms.txt
> Ask it with GET https://upstash.com/docs/ask?q=<keywords or question>. Short keywords rank best.
> Use these to discover all available pages before exploring further.
> Feature requests: https://upstash.com/docs/common/help/feature-requests.md lists what users asked for and how to vote for a request or add one.
> Page rating: `GET https://upstash.com/docs/feedback?path=/docs/blob/sdks/py/errors&helpful=true` (or `false`) records whether this page helped. An optional `comment` (URL-encoded, at most 500 characters) says what was missing or wrong. It is anonymous and needs no key. Comments hold no code, secrets or personal data.

# Errors

> BlobError in Python: the fields it carries, to_dict(), raising one from an upload handler, and what raises something else.

```python app/avatar.py
from upstash_blob import BlobError, BlobInfo, Bucket

bucket = Bucket.from_env()


def avatar(path: str) -> BlobInfo | None:
    try:
        return bucket.info(path)
    except BlobError as e:
        if e.code == "not_found":
            return None
        raise
```

Every failure the SDK reports is a `BlobError`, an `Exception` subclass. Its codes are the ones the TypeScript SDK and the browser client use, so an error raised in your Python handler reaches a React hook with its code intact. The codes, their statuses and when each one is raised are on [Errors](/blob/reference/errors).

`BlobError` and `BlobErrorCode` are exported from `upstash_blob`.

---

## Catching it

```python
try:
    bucket.put(path, body, max_size="5mb")
except BlobError as e:
    e.code     # BlobErrorCode, e.g. 'too_large'
    e.status   # int, e.g. 413
    e.message  # str, the same as str(e)
```

There is one `BlobError` class, so `except BlobError` and `isinstance` are all you need. `BlobErrorCode` is a `Literal` of every code, so an editor completes them.

---

## Fields

```python
try:
    bucket.delete(["a.png", "b.png", "c.png"])
except BlobError as e:
    if e.code == "partial_delete":
        queue_for_retry(e.failed or [])
    elif e.code == "mint_backoff":
        retry_later(after=e.retry_after)
    else:
        raise
```

| Field | Type | Set by |
| ----- | ---- | ------ |
| `code` | `BlobErrorCode` | Always. One of the [codes](/blob/reference/errors#the-codes). |
| `status` | `int` | Always. |
| `message` | `str` | Always. See [Messages](/blob/reference/errors#messages). |
| `hint` | `str \| None` | Any code. See [Hints](/blob/reference/errors#hints-fold-into-the-message). |
| `failed` | `list[str] \| None` | `partial_delete`: the paths that survived the delete. |
| `etag` | `str \| None` | `already_exists`: the etag of what is already there. |
| `size` | `int \| None` | `already_exists`: the size of what is already there. |
| `retry_after` | `float \| None` | `mint_backoff`: seconds the credential service asked the caller to wait. On another code, only when you set it yourself. |
| `reason` | `str \| None` | A refusal from Upstash meant for the bucket owner, such as a suspended or read-only bucket. Never in `message` or `to_dict()`. |

The underlying error, when there was one, is the standard `__cause__`, set with `raise ... from`. A `move_left_a_copy` carries the failed delete there.

### Built-in hints

```python
BlobError("length_required").message
# 'Length required (pass size= or max_size= so the length is known before the first byte)'
```

| Code | Built-in hint |
| ---- | ------------- |
| `signature_mismatch` | the upload did not match what was authorized, usually a different size or content type |
| `length_required` | pass `size=` or `max_size=` so the length is known before the first byte |

How a hint folds into the message is on [Hints fold into the message](/blob/reference/errors#hints-fold-into-the-message).

---

## Raising one

```python
BlobError("not_found").message                      # 'Not found'
BlobError("forbidden", "not your thread").message    # 'Not your thread'
BlobError("too_large", "you are out of space")
BlobError("rate_limited", retry_after=30)
```

`BlobError(code, message=None, *, hint, status, failed, etag, size, retry_after, reason)`. The message is positional; everything else is a keyword. Raise one from an upload handler callback to refuse an upload with that code; see [Refusing](/blob/sdks/py/upload-handler#refusing).

### to_dict()

```python
BlobError("too_large", "cat.png is 3.1 MB, over the 2 MB limit").to_dict()
# {'code': 'too_large', 'message': 'cat.png is 3.1 MB, over the 2 MB limit', 'status': 413}
```

The JSON body an upload route answers a refusal with, and what the browser client rebuilds into its own `BlobError`. `hint`, `failed`, `etag`, `size` and `retryAfter` are included when they are set. `reason` never is.

The handler writes this body for you. Call it yourself to answer with a `BlobError` from a route of your own, such as one that calls `put` for [`useServerUpload`](/blob/uploads/upload-client#useserverupload):

```python app/main.py
from fastapi.responses import JSONResponse

try:
    blob = await bucket.put(path, file.file, max_size="2mb")
except BlobError as e:
    return JSONResponse(e.to_dict(), status_code=e.status)
```

A `BlobError` also survives `pickle`, so it crosses a process pool or a task queue with its fields intact.

---

## Bad options raise TypeError or ValueError

A wrong type raises a `TypeError` and a value that does not parse raises a `ValueError`, where the option is written:

| Mistake | Raises |
| --- | --- |
| `Bucket.from_env()` with the variable unset | `TypeError` |
| `retry={"retrys": 3}`, `content_types="image/*"`, `max_size=True` | `TypeError` |
| `max_size="5mbit"`, `expires_in="soon"`, a negative size | `ValueError` |
| A path that is empty or has a `.` or `..` segment, on any `Bucket` method | `ValueError` |
| An upload handler with no `on_before_upload`, an empty `routes`, a bad route name | `TypeError`, when the handler is built |
| A body of an unsupported type, or one that yields `str` | `TypeError` |
| `cache="forever"` on `Bucket(...)` | `ValueError`, from each call that stores an object |

Two option mistakes raise a `BlobError` instead. A `content_types` list that is empty, or holds an entry that is not a `type/subtype` or one of `image/*`, `video/*` and `audio/*`, raises `invalid_content_type_pattern`, on a `put` or when a handler is built. A `content_type` that is not a media type raises `invalid_input`.

The upload handler turns the path errors into `invalid_input`, since a path built from the browser's filename can be the client's doing. A few handler mistakes only show up while a request runs, and raise a `TypeError` that the framework answers as a 500:

- `on_before_upload` returned something other than a `BeforeUploadResult`, or a `state` that is not JSON-serializable.
- `on_before_upload` returned `content_types` or a `max_size` wider than the route's; see [Narrowing per user](/blob/uploads/constraints#narrowing-per-user).
- `on_upload_complete` returned a value that is not JSON-serializable.
- An `UploadHandler` callback returned a coroutine; see [Sync or async](/blob/sdks/py/upload-handler#sync-or-async).
