Error Handling
Syncraft Labs uses an optimistic update with pessimistic rollback strategy. This guide explains every error scenario and the patterns to handle them.
The Rollback Flow
When you call update(), the following happens:
update(draft => { draft.count += 1 }) │ ▼┌─ 1. Produce nextState + patches (proxy)│├─ 2. Update memory (instant) ← UI sees the change HERE├─ 3. Notify subscribers (re-render)├─ 4. Broadcast to other tabs│├─ 5. Write to IndexedDB ─── SUCCESS ──▶ Done ✅│ ││ └── FAILURE ──▶ ROLLBACK ⚠️│ ││ ├─ Revert memory to previous state│ ├─ Re-notify subscribers (re-render)│ └─ Set error state└─ 6. Append OutboxEntry ─── FAILURE ──▶ Same rollbackKey insight: The UI updates before IndexedDB confirms the write. If the write fails, the UI reverts back — users see a brief flash of the new state, then it disappears. This is the trade-off for instant writes.
Error Sources
| Source | When | Rollback? | Retry? |
|---|---|---|---|
set() / update() | IndexedDB write fails (quota, corruption) | ✅ Yes | ❌ No (user must retry) |
pusher | Network/server error during sync | ❌ No (state is in IndexedDB) | ✅ Auto (exponential backoff) |
fetcher | Network/server error during initial fetch | ❌ No | ❌ No (call refetch()) |
hydrate() | IndexedDB read fails | ❌ No | ❌ No |
| Outbox overflow | maxOutboxSize reached | ❌ No (state write prevented) | ❌ No (sync first) |
Monitoring the error State
Both useSync (React) and useSync (Vue) return a reactive error object. When any operation fails, error is populated. When an operation succeeds, error is cleared (null).
React Example
function TodoApp() { const { data, update, refetch, error } = useSync<TodoState>("todos", opts);
return ( <div> {error && ( <div role="alert" className="error-banner"> <span>{error.message}</span> <button onClick={() => refetch()}>Try Again</button> </div> )} {/* UI components */} </div> );}Vue Example
<script setup lang="ts">import { useSync } from "@syncraft-labs/vue";
const { data, update, refetch, error } = useSync<TodoState>("todos", opts);</script>
<template> <div v-if="error" class="error-banner" role="alert"> <span>{{ error.message }}</span> <button @click="refetch">Try Again</button> </div></template>Outbox Overflow Handling
If a user works offline for a long time, the outbox might hit maxOutboxSize (default: 1000). You can configure how Syncraft Labs responds using overflowStrategy:
1. overflowStrategy: "reject" (Default)
When full, set() throws a hard Error:
const { update } = useSync("todos", { maxOutboxSize: 100, overflowStrategy: "reject", // default onOverflow: ({ outboxSize, maxOutboxSize }) => { console.warn(`Outbox overflowed (${outboxSize}/${maxOutboxSize})`); },});2. overflowStrategy: "dropOldest"
Automatically drops the oldest entry from IndexedDB with a console.warn when full, allowing new mutations to proceed:
const { update } = useSync("todos", { maxOutboxSize: 500, overflowStrategy: "dropOldest", onOverflow: ({ storageKey }) => { toast.warn("Offline storage limit reached. Oldest unsynced change was discarded."); },});3. overflowStrategy: "forceFlush"
Triggers an immediate sync attempt via onOverflow before deciding whether to allow the write:
const { update } = useSync("todos", { maxOutboxSize: 200, overflowStrategy: "forceFlush", onOverflow: async () => { // User-provided logic to flush pending entries await triggerEmergencySync(); },});Note: For
"forceFlush", ifonOverflowresolves and the outbox count is still at or abovemaxOutboxSize,set()throws anErrorindicating the flush failed to free enough space.
Sync Failure Recovery
When pusher fails (e.g., server returned 500 or network timeout):
- The outbox entries remain in IndexedDB.
- Syncraft Labs starts exponential backoff:
- Delay doubles each attempt: 1s → 2s → 4s → 8s → 16s → 32s → 60s (max)
- When
navigator.onLinefires (user comes back online), the backoff timer resets and sync attempts immediately.
You can also trigger a manual sync attempt by calling refetch():
const { refetch, isSyncing, error } = useSync<State>("key", opts);
// Manual retry button<button onClick={() => refetch()} disabled={isSyncing}> {isSyncing ? "Syncing…" : "Retry Sync"}</button>