Errors and retries

Branch on the typed error code, retry only what the envelope says is safe, and keep a retried write from running twice.

Docs menu

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 identity fields 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_key argument 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"