---
title: Errors
description: The closed set of codes, their HTTP statuses, and which are worth retrying.
---

Every non-2xx response is the same envelope, whatever the route:

```json
{ "error": { "code": "...", "message": "...", "request_id": "...", "details": [], "quota": {} } }
```

**Switch on `code`, never on `message`.** The code is a closed set with a fixed status per
member; the message is written for a person and may be reworded in any release.

## The codes

| Code                    | Status | Means                                                                                        | Retry? |
| ----------------------- | :----: | -------------------------------------------------------------------------------------------- | :----: |
| `invalid_request`       |  400   | the request was malformed or a field was wrong. `details` names the fields                   |   no   |
| `unauthenticated`       |  401   | no credential, or one the API does not accept                                                |   no   |
| `quota_exceeded`        |  402   | a plan entitlement declined it. `quota` says which                                           |   no   |
| `permission_denied`     |  403   | authenticated, but the role or scope does not allow it                                       |   no   |
| `not_found`             |  404   | no such object **in this organization**                                                      |   no   |
| `conflict`              |  409   | the current state forbids it — an illegal transition, a held lease, a reused idempotency key |   no   |
| `precondition_failed`   |  412   | something that had to be true first was not                                                  |   no   |
| `rate_limited`          |  429   | too many attempts; wait                                                                      |  yes   |
| `internal_error`        |  500   | a fault on our side                                                                          |  yes   |
| `unsupported_operation` |  501   | the deployment is not configured for this feature                                            |   no   |
| `compute_unavailable`   |  503   | the machine is not reachable yet, or the substrate is unwell                                 |  yes   |

`not_found` rather than `permission_denied` for another organization's object is deliberate: a
403 would confirm that the id exists.

## Fields

| Field        | Always?              | What it is                                                     |
| ------------ | -------------------- | -------------------------------------------------------------- |
| `code`       | yes                  | the closed set above                                           |
| `message`    | yes                  | human-readable, not machine-parseable                          |
| `request_id` | yes                  | quote it in a support request                                  |
| `details`    | on `invalid_request` | `{ field, message }`, with `field` a dotted path into the body |
| `quota`      | on `quota_exceeded`  | which limit declined this request                              |

## Handling them

```ts
import { RasterError } from "@raster/sdk";

try {
  await client.machines.create({ size: "large" });
} catch (error) {
  if (!RasterError.is(error)) throw error;

  switch (error.code) {
    case "quota_exceeded":
      // render this, not the message: it names the limit, the current value,
      // the maximum, and where to go to lift it.
      show(error.quota);
      break;
    case "invalid_request":
      for (const d of error.details) markField(d.field, d.message);
      break;
    default:
      report(error.code, error.requestId);
  }
}
```

```python
from raster import RasterError

try:
    client.machines.create(size="large")
except RasterError as error:
    if error.code == "invalid_request":
        for d in error.details:
            mark_field(d["field"], d["message"])
    else:
        report(error.code, error.request_id)
```

`error.status` is 0 on a transport failure — a timeout, a DNS failure, a proxy that answered
instead of the API — and `request_id` is null for the same reason. Anything the API itself
produced has both.

:::note
`quota` is surfaced as `error.quota` in the TypeScript SDK. The Python SDK does not expose it as
an attribute yet; read `error.message`, or the raw body via `client.api`.
:::

## `compute_unavailable` is usually not a failure

A machine that is `running` but whose agent has not published its address yet answers every
desktop route with `compute_unavailable`. That happens on **every boot**. It is why both SDKs
treat the code as retryable and why `waitForDesktop` swallows it while polling.

Treat it as "not yet", not as "broken", and put a deadline on the waiting rather than a retry
count.

## Gateway close codes

A websocket failure is not this envelope — it is a close code. See
[the realtime gateway](/docs/reference/realtime#close-codes). They map onto the same vocabulary:
`4401` is `unauthenticated`, `4403` is `permission_denied`, `4404` is `not_found`, `4409` is
`conflict`, `4429` is `rate_limited`, and `4503` is `compute_unavailable`.
