Error Handling
Every user-state write is queued and drained in order. When the server responds, the SDK classifies the result into one of the tags below, which decides whether the write is retried, dropped, or committed, and what happens to the drain queue.
Write-result contract
| Failure kind | API status or code | SDK tag | Drain behavior | 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 | rejected | Dropped via shiftAndRebuild() | No |
| Transient failure | 5xx, no_response/network error, 429 rate_limited, 404 configuration, any other unrecognized status or unparseable 400 body | retry | Kept queued | Yes, standard backoff |
| Quota exceeded | 402 | quota | Kept queued | Yes, slower quota pace: jittered ~2.5-5 min base, doubling to a 30-minute cap. Never halted |
Authorization rejection ('real' verdict) | 401/403 | authz | Session reset: queue and optimistic state are cleared, and the stored credential is cleared | No |
Authorization rejection ('stale' verdict) | 401/403 | authz | Kept queued | Yes, standard backoff |
| Version conflict | 412 | conflict | Transparent refetch-and-replay, no error published | Not applicable |
| Committed but unverified | Malformed 2xx envelope after a PATCH | committed-unverified | Treated as committed: head removed, version cleared to force a 412 reconcile on the next write. Surfaced once as a protocol error | Never resent, never rolled back |
| Confirmed no-op | patch_path_not_found | noop | Shifted silently | No error |
The 'real' vs 'stale' distinction for a 401/403 depends on whether the credential that was rejected is still the client's current credential at the time the rejection is processed. A 'real' verdict means the client's session is genuinely no longer authenticated, so the session resets. A 'stale' verdict means the rejection was earned by a token that has since been superseded (for example by a concurrent refresh), so it carries no information about the current session and the write simply retries.
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 or protocol (committed-unverified) error, passing the SkyStateError so the application can react without polling the snapshot.