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 kind | API status or code | What happens | Retried? |
|---|---|---|---|
| Content rejection | 400 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 reported | No |
| Transient failure | 5xx, no_response (network error or timeout), 429 rate_limited, 404 configuration, any other unrecognized status or unparseable 400 body | Kept pending | Yes, standard backoff |
| Quota exceeded | 402 | Kept pending | Yes, on the slower quota pace described in Retries and backoff. Never halted |
| Authorization rejection, current credential | 401/403 | Session reset: pending writes and optimistic state are cleared, and the stored credential is cleared | No |
| Authorization rejection, superseded credential | 401/403 | Kept pending | Yes, standard backoff |
| Version conflict | 412 | Transparent refetch-and-replay, no error published | Not applicable |
| Committed but unverified | A 2xx write response whose body the SDK cannot interpret | Treated as committed and never rolled back; the next write reconciles against the server version. Reported once as a protocol error | Never resent |
| Confirmed no-op | patch_path_not_found (clearing a key the server already lacks) | Completed silently | No 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, a401/403on a signed-in request, or a write attempted without sign-inconfiguration- unknown account, project, or environment slug (404); retried, so a fixed slug heals without a reloadquota- monthly request quota or tier limit exceeded (402); see Retry-After for what the response carriesrate_limited- per-minute rate limit exceeded (429)server_unavailable-5xxresponse; retried on the SDK's own backoff. The SDK does not readRetry-Afteron5xx; see Retry-Afterprotocol- a response the SDK could not interpret: an unexpected status or a malformed bodyno_response- no HTTP response at all (offline, DNS failure, the SDK's 10-second request timeout, CORS); retriedmissing_config,missing_provider,disposed- programmer misuse; thrown synchronously at the call siteinvalid_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 viaonError(seeuseStatus().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 triggersunsupported_media_type- the server rejected the write'sContent-Type(415). Classified from the HTTP status alone; the response body is not read. SeeuseStatus().healthfor how this differs from thepatch_*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 onno_responseand the misuse codes. Onauthenticationit isnumber | null, because auth can fail before any HTTP response is receivedpath- the user-state key a keyed write was for; always set on write rejections andinvalid_path,string | nullon other request errors, absent on the sync-thrown misuse codesretryAfter- onrate_limitedonly: the server'sRetry-Afterdelay in seconds, ornullwhen 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.