Skip to content

Error Handling ​

User-state writes are sent in the order they were made. When the server responds, the SDK decides whether the write is retried, dropped, or treated as committed, and whether an error is reported.

Write-result contract ​

Failure kindAPI status or codeWhat happensRetried?
Content rejection400 patch_invalid, patch_unsupported_operation, patch_invalid_path, patch_missing_value, patch_path_untraversable, patch_invalid_array_index, patch_invalid_state_root, patch_state_too_large; 415 unsupported_media_type (classified from the status alone; the body is not read)Dropped; its optimistic value is rolled back and the error is reportedNo
Transient failure5xx, no_response (network error or timeout), 429 rate_limited, 404 configuration, any other unrecognized status or unparseable 400 bodyKept pendingYes, standard backoff
Quota exceeded402Kept pendingYes, on the slower quota pace described in Retries and backoff. Never halted
Authorization rejection, current credential401/403Session reset: pending writes and optimistic state are cleared, and the stored credential is clearedNo
Authorization rejection, superseded credential401/403Kept pendingYes, standard backoff
Version conflict412Transparent refetch-and-replay, no error publishedNot applicable
Committed but unverifiedA 2xx write response whose body the SDK cannot interpretTreated as committed and never rolled back; the next write reconciles against the server version. Reported once as a protocol errorNever resent
Confirmed no-oppatch_path_not_found (clearing a key the server already lacks)Completed silentlyNo error

Whether a 401/403 resets the session depends on whether the rejected credential is still the client's current credential when the response is processed. If it is, the session is genuinely no longer authenticated and the SDK resets it. If the credential had already been replaced (for example by a refresh that completed while the request was in flight), the rejection says nothing about the current session and the write is simply retried.

Optimistic rollback ​

A rejected write reverts only its own optimistic value. Other pending writes for the same or different keys stay applied. The resulting error snapshot's data field carries the rolled-back value, so consumers reading the snapshot after the rejection see the pre-write state for that key.

Error snapshot and onError ​

The user-state snapshot exposes data (the current value, reflecting any rollback) alongside error (the SkyStateError that caused the most recent rejection, if any). In the React SDK, onError fires once per rejected write and once per committed-but-unverified (protocol) error, passing the SkyStateError so the application can react without polling the snapshot.

Error codes ​

The SDK translates failures into typed SkyStateError values with a code: SkyStateErrorCode field. This is the full code list and HTTP-status-to-code mapping; other pages link here rather than repeating it.

  • authentication - token refresh rejected, a 401/403 on a signed-in request, or a write attempted without sign-in
  • configuration - unknown account, project, or environment slug (404); retried, so a fixed slug heals without a reload
  • quota - monthly request quota or tier limit exceeded (402); see Retry-After for what the response carries
  • rate_limited - per-minute rate limit exceeded (429)
  • server_unavailable - 5xx response; retried on the SDK's own backoff. The SDK does not read Retry-After on 5xx; see Retry-After
  • protocol - a response the SDK could not interpret: an unexpected status or a malformed body
  • no_response - no HTTP response at all (offline, DNS failure, the SDK's 10-second request timeout, CORS); retried
  • missing_config, missing_provider, disposed - programmer misuse; thrown synchronously at the call site
  • invalid_path - an invalid state key or non-JSON write value. Detected at the call site it throws synchronously; a deferred updater failure caught at save time is instead reported via onError (see useStatus().health)
  • patch_invalid, patch_unsupported_operation, patch_invalid_path, patch_missing_value, patch_path_untraversable, patch_invalid_array_index, patch_invalid_state_root, patch_state_too_large - the server rejected that specific write (400). See Patch rejection codes for the per-code triggers
  • unsupported_media_type - the server rejected the write's Content-Type (415). Classified from the HTTP status alone; the response body is not read. See useStatus().health for how this differs from the patch_* write rejections

A 204 (no state yet) is not an error: SDK keyed reads fall back to their fallback value.

There is no conflict error code: a 412 version race is reconciled inside the SDK (refetch and replay) and never surfaces as an error.

Error fields ​

Alongside code and message, variants carry extra fields:

  • httpStatus - on errors that arrived as an HTTP response; absent on no_response and the misuse codes. On authentication it is number | null, because auth can fail before any HTTP response is received
  • path - the user-state key a keyed write was for; always set on write rejections and invalid_path, string | null on other request errors, absent on the sync-thrown misuse codes
  • retryAfter - on rate_limited only: the server's Retry-After delay in seconds, or null when the header was absent or unreadable

message text is not stable across releases; branch on code, never on message.

Retries and backoff ​

Retrying applies to the background loops: public-state loads, user-state reads and writes, and token refresh. The one-shot code exchange during sign-in is not retried, so a failure there surfaces once and halts the callback.

Retries back off with jitter: the first retry comes roughly 1-5 seconds after the failure, rising to a steady state of roughly 2.5-5 minutes between attempts. quota errors are paced slower: a jittered 2.5-5 minutes at first, doubling up to a 30-minute ceiling. A rate_limited response honors the server's Retry-After, capped at 30 minutes.