Errors & Retries
Retry transport safely and turn business failures into repair actions
Use HTTP status for transport class and structured code, issues, and
context for program behavior. Never parse message.
| Failure | Default action |
|---|---|
| Schema or business validation | Correct payload; do not retry unchanged |
| Authentication | Correct credential or expiry; do not rotate teams automatically |
| Permission or scope | Correct access assignment or selected team |
| Lock or conflict | Re-read current state and show documented recovery |
429 | Respect Retry-After; webhook administration enforces quotas |
Transient 5xx / availability | Retry idempotently with jitter |
| External provider failure | Read connection/sync state before another action |
| Webhook recipient failure | Follow durable delivery retry and replay contract |
Webhook subscription, test, and replay operations can return 429 when their
documented quota is exceeded. Respect Retry-After and retry with bounded
backoff. Other API surfaces do not currently enforce application-level quotas,
although infrastructure can still return a rate-limit response.
Idempotency
Use stable external references where the operation documents them. A retry must not invent a new source identity or persist IDs returned by dry run. For a write whose outcome is uncertain, read current state before retrying when no idempotency key exists.
The TypeScript client (@vrplatform/api) automatically retries GET, HEAD,
and OPTIONS requests with up to five attempts by default. Set attempts to
change that read limit, including attempts: 1 to disable read retries.
Writes are attempted once, even when attempts is larger. Custom assertResponse
validation runs on that attempt unless assertOnLastAttempt is explicitly false.
Assertion failures never replay the write. There is no automatic write-retry opt-in.
Reconcile an uncertain write before another request; a session ID does not deduplicate
a business operation.
TypeScript Error Handling
throwIfError(response) and await throwIfError(responsePromise) preserve
the same public envelope in ApiClientError.error. The error also exposes
code, issues, context, links, and retryable directly. Narrow the
unknown context before reading recovery fields, or use the endpoint's
generated error response type.
An HTTP failure with malformed JSON, an empty body, or a missing public error
envelope throws ApiClientResponseError, whose status is the HTTP status.
It has no public error code. A 503 alone does not identify worker resource
exhaustion. Transport rejections remain transport errors.
Backoff
Bound retries, add jitter, and stop on deterministic 4xx failures. Alert on
exhaustion with correlation ID, route, team, structured error code, and safe
context. Never log API keys, bearer tokens, or webhook secrets.
An exhausted database socket connection attempt returns 503 SERVICE_UNAVAILABLE
with the static message Database connection is unavailable. This response does
not include a blanket retryable flag or Retry-After header. Retry reads with
bounded backoff; reconcile a write's outcome before repeating it.
For a known database connection loss after dispatch, including a severed
Hyperdrive egress response or an established connection loss, the API retries
only compiled select operations. Mutations and raw or unknown queries remain
single-attempt because their outcome may be unknown. A caller receiving
503 SERVICE_UNAVAILABLE must therefore reconcile a write before retrying it
unless the complete operation is idempotent or has stable source identity.
Webhook delivery uses its own documented application schedule; do not add a second aggressive retry loop at the recipient.
See Error Contract and Webhooks.
