Branch on the code, not the message
Every failed call answers the same envelope: error (text for a person), code (the stable name to branch on), status, retryable, next_action, and an optional fix naming the call to make next.
{
"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: a transaction conflict, rate limiting, a concurrency limit, or server load or a timeout. Retry those within your own deadline, with backoff.
A validation error (INVALID_ARGUMENT), a permission error, or a schema violation fails the same way every time until you change the request: do not retry it unchanged. A write guarded by an expected _version that lost a race needs a fresh read before the next attempt, not a blind retry.
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 answers the version that already holds it (
existed: true) rather than a new one, so a retried push after a dropped response never double-ships. - An operation that takes its own
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
A function's throw is recorded with its parsed stack frames and returned as function_error alongside the usual envelope on a failed function.invoke or function.execute_inline. Read it back 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"