---
title: Errors and retries
description: RasterError, the closed code set, idempotency, retries and pagination.
---

## RasterError

One exception class, in both languages.

<CodeGroup>

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

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

  error.code; // a closed set — switch on this
  error.status; // http status; 0 on a transport failure
  error.requestId; // present on anything the api produced
  error.details; // [{ field, message }] when the body was the problem
  error.quota; // which limit declined it, on quota_exceeded
  error.retryable;
}
```

```python Python
from raster import RasterError

try:
    client.machines.create(size="large")
except RasterError as error:
    error.code         # a closed set — branch on this
    error.status       # http status; 0 on a transport failure
    error.request_id   # present on anything the api produced
    error.details      # [{"field", "message"}] when the body was the problem
    error.retryable
```

</CodeGroup>

**Switch on `code`, never on `message`.** The message is written for a person and may be reworded
in any release. The full code table is in [errors](/docs/reference/errors).

:::note
`error.quota` is TypeScript only. The Python SDK does not surface it as an attribute yet — read
`error.message`, which names the limit in prose.
:::

## Quota errors

<CodeGroup>

```ts TypeScript
if (error.code === "quota_exceeded") {
  error.quota.limit; // "running_machines"
  error.quota.current; // 3
  error.quota.maximum; // 3
  error.quota.plan_id; // "developer"
  error.quota.upgrade_url;
}
```

```python Python
if error.code == "quota_exceeded":
    print(error.message)   # names the limit in prose
```

</CodeGroup>

In TypeScript, render `error.quota` rather than the message: it names the limit, the current value,
the maximum and where to go to lift it. A caller that shows the sentence instead makes the customer
guess which of their limits it was.

## `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`](/docs/sdk/machines#running-is-not-ready) swallows it while
polling.

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

## Retries

The transport retries a request only when repeating it can produce nothing but the effect it
already had: a `GET`, or an unsafe request **carrying an idempotency key**.

Retried codes are `rate_limited`, `internal_error` and `compute_unavailable`, with exponential
backoff to a 2 second ceiling. Everything else is attempted once.

The per-attempt timeout is 30 seconds by default. A machine that is still booting is **refused, not
waited on** — waiting is `waitForDesktop`'s job, not the transport's.

## Idempotency

<CodeGroup>

```ts TypeScript
await client.machines.create({
  name: "research",
  idempotencyKey: crypto.randomUUID(),
});
```

```python Python
client.machines.create(name="research", idempotency_key=str(uuid.uuid4()))
```

</CodeGroup>

Every unsafe call takes one. A repeat replays the first response instead of creating a second
machine, which is also what makes the call retryable at all.

The stored record holds a hash of the method, path and body, so reusing a key with a **different**
body is a `conflict` rather than a wrong replay. Keys are retained for 24 hours.

## Pagination

Every list walks pages for you, so a cursor never appears in your code:

<CodeGroup>

```ts TypeScript
for await (const machine of client.machines.list()) {
  // every page
}

import { collect } from "@raster/sdk";
const all = await collect(client.machines.list());
```

```python Python
for machine in client.machines.list():
    ...   # every page

from raster import collect
all_machines = collect(client.machines.list())
```

</CodeGroup>

`collect` has a 10,000-item ceiling so a bug cannot run away; pass a second argument to change it.

Lists take `limit` to size a page and `cursor` to resume from one. A cursor is **opaque** — pass
back what a page returned and never construct one. An offset would skip or repeat rows on a list
that changes while it is being read.

## Transport failures

A timeout, a DNS failure, or a proxy that answered instead of the API is still a `RasterError`,
with `status: 0` and a null request id. Anything the API itself produced has both.

In TypeScript, your own `AbortSignal` is yours: aborting through it rejects with your reason, not a
`RasterError`. Only the SDK's own deadline becomes a product error.
