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 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 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 returns the existing version (existed: true). A retry after a dropped response does not create a second version.
  • An operation that takes an 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

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"