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

# Errors

> Every error code, and which are worth retrying

```json theme={null}
{
  "code": "dataset_too_large",
  "category": "validation",
  "problem": "The dataset exceeds the V1 size ceiling.",
  "cause": "Rows x columns are above the benchmarked limit this engine can serve.",
  "fix": "Reduce the dataset below the published max rows/columns and retry.",
  "docUrl": "https://hollerith.dev/docs/errors#dataset_too_large",
  "retryable": false,
  "requestId": "req_9f2c41ab7d0e5c83"
}
```

That is the envelope, returned with the HTTP status listed for the code. There are 26 codes,
defined once in a shared contract and mirrored by the Python SDK.

## The envelope

* **`code`** — the stable identifier, one of 26. The only field to branch on.
* **`category`** — one of eight. It selects the SDK exception class.
* **`problem`, `cause`, `fix`** — what went wrong, why, and what to do. Written for a human.
* **`docUrl`** — the documentation anchor for this code.
* **`retryable`** — `true` for four codes. See [Retrying](#retrying).
* **`requestId`** — `req_` plus 16 hex characters, logged server-side under the same id.

Branch on `code`. Never branch on message text — `problem`, `cause` and `fix` are written for
a human and the server rewrites them per instance, so a malformed CSV names the offending row
and column in its `cause`.

## Not every error is an envelope

```json theme={null}
{"error": "invalid_json"}
```

A request body that fails to parse, or that matches none of the accepted shapes, returns a
bare `400` like that one — or `{"error":"invalid_request"}`. No `code`, no `category`, no
`requestId`.

So "every error carries a request id" is false. Typed envelopes carry one; malformed input
does not, and there is nothing to quote to support.

* **The SDK cannot type these.** With no `code` to resolve, it falls back to the base
  `HollerithError` carrying the text `An unexpected error occurred.` An
  `except ValidationError` block will not catch it.
* **Catch `HollerithError` at the outer edge** of any integration, not only the subclasses you
  expect.
* **A `400` with no code means the body shape was wrong**, most often a misspelled field. A
  typo in the inline `POST /v1/predictions` body falls through to the context-backed parser
  and returns `invalid_request` without saying which shape failed.

## Categories and SDK exceptions

The `category` field, not the code, selects the exception class. Eight categories, eight
classes, all subclassing `HollerithError`.

| Category    | HTTP     | Exception                 |
| ----------- | -------- | ------------------------- |
| auth        | 401      | `AuthenticationError`     |
| permission  | 403      | `PermissionDeniedError`   |
| quota       | 429      | `QuotaExceededError`      |
| validation  | 413, 422 | `ValidationError`         |
| not\_found  | 404      | `NotFoundError`           |
| conflict    | 409      | `ConflictError`           |
| unavailable | 503      | `ServiceUnavailableError` |
| server      | 500      | `ServerError`             |

```python theme={null}
from hollerith import HollerithError, QuotaExceededError, ValidationError

try:
    preds = clf.predict(rows)
except ValidationError as exc:
    print(exc.code, exc.fix)        # e.g. schema_mismatch
except QuotaExceededError as exc:
    print(exc.retryable)            # False for quota_exceeded
except HollerithError as exc:
    print(exc.request_id)           # None when the SDK raised it locally
```

Every exception exposes `code`, `problem`, `cause`, `fix`, `doc_url`, `retryable` and
`request_id`. Printing one renders all of them on separate lines.

## The catalogue

The `What to do` column is the `fix` string the API returns for that code.

### auth — 401, `AuthenticationError`

| Code              | Category | HTTP | Retryable | What to do                                                                        |
| ----------------- | -------- | ---- | --------- | --------------------------------------------------------------------------------- |
| `missing_api_key` | auth     | 401  | no        | Set `HOLLERITH_API_KEY`, or pass `api_key=` to the client.                        |
| `invalid_api_key` | auth     | 401  | no        | Copy the key again from the console (it is shown once at creation) and retry.     |
| `revoked_api_key` | auth     | 401  | no        | Create a new key on the API Keys page and update your environment.                |
| `unauthenticated` | auth     | 401  | no        | Sign in to the console and retry; this surface requires an authenticated session. |

### permission — 403, `PermissionDeniedError`

| Code                    | Category   | HTTP | Retryable | What to do                                                              |
| ----------------------- | ---------- | ---- | --------- | ----------------------------------------------------------------------- |
| `org_forbidden`         | permission | 403  | no        | Use an ID created by your own org; cross-org access is never permitted. |
| `subscription_required` | permission | 403  | no        | Contact your Hollerith administrator to change this workspace plan.     |

### quota — 429, `QuotaExceededError`

| Code             | Category | HTTP | Retryable | What to do                                                                     |
| ---------------- | -------- | ---- | --------- | ------------------------------------------------------------------------------ |
| `quota_exceeded` | quota    | 429  | no        | Wait for the daily reset shown in the usage panel, or reduce the request size. |
| `rate_limited`   | quota    | 429  | yes       | Back off and retry after a short delay.                                        |

### validation — 413 and 422, `ValidationError`

| Code                          | Category   | HTTP | Retryable | What to do                                                            |
| ----------------------------- | ---------- | ---- | --------- | --------------------------------------------------------------------- |
| `dataset_too_large`           | validation | 422  | no        | Reduce the dataset below the published max rows/columns and retry.    |
| `payload_too_large`           | validation | 413  | no        | Reduce the dataset below the published maximum upload size and retry. |
| `malformed_csv`               | validation | 422  | no        | Check the named row/column, fix the delimiter or encoding, and retry. |
| `schema_mismatch`             | validation | 422  | no        | Send prediction rows with the same feature columns used to fit.       |
| `missing_target_column`       | validation | 422  | no        | Pass `target=` a column that exists in the DataFrame, then retry.     |
| `unsupported_task`            | validation | 422  | no        | Use a classification or regression target.                            |
| `fitted_context_incompatible` | validation | 422  | no        | Call `fit()` again with the current SDK/backend before predicting.    |

`fitted_context_incompatible` is a `validation` error, not a `not_found` one, despite its name
sitting beside three context codes below. It raises `ValidationError`.

### not\_found — 404, `NotFoundError`

| Code                       | Category   | HTTP | Retryable | What to do                                                             |
| -------------------------- | ---------- | ---- | --------- | ---------------------------------------------------------------------- |
| `job_not_found`            | not\_found | 404  | no        | Use a job id returned by this client for your org.                     |
| `training_ref_expired`     | not\_found | 404  | no        | Call `fit()` again to re-stage the training set, then predict.         |
| `fitted_context_not_found` | not\_found | 404  | no        | Use a fitted context id returned by `fit()` for this organization.     |
| `fitted_context_expired`   | not\_found | 404  | no        | Call `fit()` again to prepare a fresh context, then retry `predict()`. |

Reading `/v1/predictions/{id}/result` before the job succeeds returns `job_not_found`, not a
409 or a 425. Poll the job until its status is `succeeded`, then read the result.

### conflict — 409, `ConflictError`

| Code                   | Category | HTTP | Retryable | What to do                                                             |
| ---------------------- | -------- | ---- | --------- | ---------------------------------------------------------------------- |
| `idempotency_conflict` | conflict | 409  | no        | Use a fresh idempotency key, or resend the identical original request. |

### unavailable — 503, `ServiceUnavailableError`

| Code                 | Category    | HTTP | Retryable | What to do                                                                                                                            |
| -------------------- | ----------- | ---- | --------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `worker_warming_up`  | unavailable | 503  | yes       | The SDK waits and retries while polling a `predict`, `evaluate` or `forecast`. A `fit` wait only does so when you pass `on_warming=`. |
| `worker_unavailable` | unavailable | 503  | yes       | Retry shortly.                                                                                                                        |
| `cache_unavailable`  | unavailable | 503  | yes       | Retry shortly; if it persists, call `fit()` again to rebuild the context.                                                             |

`worker_warming_up` is defined in the contract but nothing in the control plane emits it. A
cold or unreachable worker reaches you as `worker_unavailable` instead, which the SDK also
raises itself when it cannot reach the API or object storage.

### server — 500, `ServerError`

| Code                 | Category | HTTP | Retryable | What to do                                                             |
| -------------------- | -------- | ---- | --------- | ---------------------------------------------------------------------- |
| `worker_oom`         | server   | 500  | no        | Reduce the dataset size and retry; if it persists, contact support.    |
| `artifact_too_large` | server   | 500  | no        | Re-fit with fewer training rows, then predict against the new context. |
| `internal_error`     | server   | 500  | no        | Retry; if it persists, contact support with the `requestId`.           |

## Retrying

Exactly four codes are retryable: `rate_limited`, `worker_warming_up`, `worker_unavailable`
and `cache_unavailable`. Retry those, unchanged. Every other code will return the same answer
however many times you send it.

`quota_exceeded` is a `429` that is not retryable. A client that retries on status code rather
than on the `retryable` field will loop against it until the daily reset.

**The SDK has no backoff-retry layer.** What it does is narrower than that, in three ways:

* **Only `ServiceUnavailableError` with `retryable=true`** is absorbed. A `QuotaExceededError`
  is raised to you on the first occurrence.
* **Only inside a poll loop.** A 503 on the call that submits the job reaches you; a 503 while
  polling an already-submitted job is swallowed.
* **At a fixed `poll_interval`**, default `1.0` second, with no exponential growth, until
  `timeout` (default `900.0` seconds) lapses and a `TimeoutError` is raised.

One asymmetry to know: while `predict()` and `evaluate()` always absorb a retryable 503 during
polling, `fit()` only does so when you pass `on_warming=`. Without that hook, a transient
warm-up during a `fit` wait propagates.

```python theme={null}
clf.fit(train, target="churned", on_warming=lambda e: print(e.problem))
```

Rate limiting and daily quota are yours to handle. Nothing in the SDK spaces out your calls.

## Two codes that carry more than one meaning

### `training_ref_expired`

One code, two situations: the staged input for a job expired before the worker read it, and a
succeeded job whose result aged out. The first needs a re-run, the second means you read the
result too late.

Scored predictions and evaluation metrics live for 1 hour. Fetch them inside that window or
re-run the job.

### `rate_limited`

This code is currently never emitted. No rate limiter is wired for the public API, so it is
defined and typed but unreachable today.

Handle it anyway. It costs one branch, and its `retryable` flag is `true` when it does arrive.

## When you contact support

Send the `requestId`. The API logs the same id server-side against the failing request, so it
is the fastest way to the exact log line.

```
An unexpected error occurred.
  Cause: The server hit a condition it did not handle.
  Fix:   Retry; if it persists, contact support with the requestId.
  Docs:  https://hollerith.dev/docs/errors#internal_error
  Request: req_9f2c41ab7d0e5c83
```

Two cases have no id to send. A bare `invalid_json` or `invalid_request` response carries none,
and errors the SDK raises before any request — `missing_api_key` is the common one — have
`request_id` set to `None`. Quote the code and the call you made instead.

## Related

* [Limits](/reference/limits) — the ceilings behind `dataset_too_large` and `payload_too_large`
* [The fitted context](/concepts/fitted-context) — expiry, and the three context codes
* [Usage and billing](/account/usage-and-billing) — what `quota_exceeded` is counting
* [Troubleshooting](/help/troubleshooting) — symptoms rather than codes
