Branch on the code, not the message
Every failed call returns an error envelope with error (a human-readable message), code (the stable name to branch on), status, retryable, next_action, and an optional fix identifying the next call to make.
{
"error": "the collection 'orders' does not exist",
"code": "COLLECTION_NOT_FOUND",
"status": 404,
"retryable": false,
"next_action": "refresh_resource",
"fix": { "operation": "collection.list" }
}
const res = await fetch(url, opts);
if (!res.ok) {
const body = await res.json();
if (body.code === "COLLECTION_NOT_FOUND") {
// call body.fix.operation (collection.list) to refresh what exists
}
}
error's text can change without notice; code does not. See api/errors for the full code list and the code-family ranges.
What is retryable
retryable: true appears only for a transient condition, such as a transaction conflict, rate limit, concurrency limit, server load, or timeout. Retry within your deadline, with backoff.
A validation error (INVALID_ARGUMENT), permission error, or schema violation requires a change to the request. Do not retry it unchanged. If a write guarded by an expected _version conflicts with another write, read the record again before the next attempt.
Idempotency
Make a write safe to retry before you retry it:
- A collection declared with
identityfields resolves a repeated insert with the same identity to the same record instead of creating a duplicate; see data-modeling. - Pushing the same version content twice returns the existing version (
existed: true). A retry after a dropped response does not create a second version. - An operation that takes an
idempotency_keyargument returns the same recorded result for a retry carrying the same key.
Backoff and Retry-After
A 429 carries Retry-After; wait at least that long before the next attempt, and back off further (exponential, with jitter) on a repeated 429 or 503. Never retry in a tight loop: it extends the condition that produced the 429 in the first place.
async function callWithBackoff(fn, attempts = 5) {
for (let i = 0; i < attempts; i++) {
const res = await fn();
if (res.status !== 429 && res.status !== 503) return res;
const retryAfter = Number(res.headers.get("retry-after")) || 2 ** i;
await new Promise((r) => setTimeout(r, retryAfter * 1000));
}
throw new Error("exhausted retries");
}
Function errors and logs
When a function throws, the error and its parsed stack frames are recorded. A failed function.invoke or function.execute_inline returns it as function_error alongside the usual envelope. Read it by request, function, or revision with function.errors; see function-errors.
curl -s "$SUBSTRIO_URL/v1/_function_errors?function=my_func&limit=5" \
-H "Authorization: Bearer $SUBSTRIO_KEY"