Errors and retries
RasterError, the closed code set, idempotency, retries and pagination.
RasterError
One exception class, in both languages.
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;
}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.retryableSwitch 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.
Quota errors
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;
}if error.code == "quota_exceeded":
print(error.message) # names the limit in proseIn 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 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
await client.machines.create({
name: "research",
idempotencyKey: crypto.randomUUID(),
});client.machines.create(name="research", idempotency_key=str(uuid.uuid4()))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:
for await (const machine of client.machines.list()) {
// every page
}
import { collect } from "@raster/sdk";
const all = await collect(client.machines.list());for machine in client.machines.list():
... # every page
from raster import collect
all_machines = collect(client.machines.list())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.