fix(query-sync-storage-persister): recover from throttle errors instead of permanently disabling persistence#11023
Conversation
…ad of permanently disabling persistence If the persist callback throws (e.g. a `retry` handler such as the built-in `removeOldestQuery` throwing on malformed persisted state), the internal `throttle()` helper never resets its `timer`, silently disabling all future `persistClient()` calls for the rest of the session. Guard the call with try/catch/finally so a single failed attempt can't permanently brick persistence.
📝 WalkthroughWalkthroughThis PR fixes the ChangesThrottle Error Recovery Fix
Estimated code review effort: 2 (Simple) | ~10 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant Persister as createSyncStoragePersister
participant Timer as ThrottleTimer
participant Storage
Client->>Persister: persistClient()
Persister->>Timer: invoke throttled func
Timer->>Storage: setItem(...)
Storage-->>Timer: throws error
Timer->>Timer: catch error, finally reset timer to null
Client->>Persister: persistClient() (next call)
Persister->>Timer: invoke throttled func
Timer->>Storage: setItem(...)
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/query-sync-storage-persister/src/index.ts (1)
113-116: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider surfacing swallowed errors for observability.
The empty
catchsilently discards all errors fromfunc, including repeatedstorage.setItem/retryfailures. Since this mirrors the async persister's behavior it's likely intentional, but callers currently have no way to detect a persistently failing storage backend.♻️ Optional: log the caught error
- try { - func(...params) - } catch { - // Prevent an error thrown by `func` (e.g. a `retry` handler) from - // leaving `timer` stuck, which would silently disable all future - // persist attempts for the rest of the session. - } finally { + try { + func(...params) + } catch (error) { + // Prevent an error thrown by `func` (e.g. a `retry` handler) from + // leaving `timer` stuck, which would silently disable all future + // persist attempts for the rest of the session. + if (process.env.NODE_ENV !== 'production') { + console.error(error) + } + } finally {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/query-sync-storage-persister/src/index.ts` around lines 113 - 116, The empty catch in the persister flow silently swallows errors from func, making repeated storage.setItem/retry failures invisible. Update the catch in the timer/reset logic in query-sync-storage-persister’s main persistence path to optionally surface the caught error through the existing logger or a similar observability hook, while still preserving the current behavior of clearing timer and avoiding session-wide failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/query-sync-storage-persister/src/index.ts`:
- Around line 113-116: The empty catch in the persister flow silently swallows
errors from func, making repeated storage.setItem/retry failures invisible.
Update the catch in the timer/reset logic in query-sync-storage-persister’s main
persistence path to optionally surface the caught error through the existing
logger or a similar observability hook, while still preserving the current
behavior of clearing timer and avoiding session-wide failure.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4aa8a398-bdb3-4ee1-862f-2887a382c96a
📒 Files selected for processing (3)
.changeset/sync-storage-persister-throttle-error-recovery.mdpackages/query-sync-storage-persister/src/__tests__/storageIsFull.test.tspackages/query-sync-storage-persister/src/index.ts
🎯 Changes
createSyncStoragePersister's internalthrottle()helper does not guard against the wrappedpersistClientcallback throwing:If
functhrows — which happens whenever theretryoption (including the built-inremoveOldestQuery, which does[...persistedClient.clientState.mutations]and throws on malformed/undefinedclientState) throws while trying to recover from a failedstorage.setItem—timeris left non-null forever. Every subsequentpersistClient()call then seestimer !== nulland silently no-ops, permanently disabling persistence for the rest of the page session with no error surfaced anywhere.The async counterpart (
@tanstack/query-async-storage-persister'sasyncThrottle) already guards against this with atry/catch— this brings the sync version in line.try/catch/finallysotimeris always reset, and the thrown error doesn't escape as an unhandled exception either (verified: without thecatch, the error propagates out of thesetTimeoutcallback and shows up as an unhandled rejection/exception).storageIsFull.test.ts) that reproduces the stuck state.Testing notes
I wasn't able to run
nx-based commands (pnpm run test:pr/nx run @tanstack/query-sync-storage-persister:test:lib) in my local environment — this repo's config files rely on symlinks (per the note in CONTRIBUTING.md about symlink support), and on my Windows checkout they materialized as plain text files instead of real symlinks, which breaks thetsc --buildstep nx depends on for this package's dependency graph. This is an environment limitation, unrelated to the change itself.As a substitute, I verified the fix by running the package's tests directly with
vitest runinsidepackages/query-sync-storage-persister:src/index.tsfix reverted (test kept): the new test fails exactly as expected (stuck timer / unhandled exception), confirming the test actually catches the regression.I'd appreciate it if a maintainer (or CI) could confirm with the full
nxpipeline on a symlink-capable environment.✅ Checklist
pnpm run test:pr. (see "Testing notes" above — ran the package's vitest suite directly instead, due to a local symlink/environment limitation)🚀 Release Impact