fix: don't throw on invalid Dates when serializing the cache - #209
Merged
dborysov merged 1 commit intoAug 5, 2026
Merged
Conversation
An invalid Date is still `instanceof Date`, but its time value is NaN and toISOString() throws RangeError: Invalid time value on it. Every Date branch in serialization called toISOString() unguarded, so a single invalid Date anywhere in the inspected cache made serialization throw. sync.ts runs in the page's MAIN world as a QueryCache subscriber, and QueryCache.notify iterates subscribers without catching, so the RangeError unwound out of the inspected application's own React commit phase and tripped its error boundary — the devtools crashed the app it was inspecting. Invalid Dates now encode as "Invalid Date", which decodes back to an equally invalid Date, and export emits new Date(NaN).
dborysov
approved these changes
Aug 5, 2026
dborysov
left a comment
Collaborator
There was a problem hiding this comment.
Nice catch, and thanks for the detective work here — the stack trace and write-up made this a really easy review. Merging as-is.
On your two questions: keep the helpers, they're much nicer than repeating the same check in four places. And stick with "Invalid Date" — the clean round-trip is worth more than a tidier wire value.
I'll follow up with a small change to make the sync script more defensive in general, since invalid dates turned out not to be the only thing that can trip it up. Nothing needed from you — thanks again for the thorough fix!
I will submit the new version somewhere today or tomorrow
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
An invalid
Dateis stillinstanceof Date, but its time value isNaNandtoISOString()throwsRangeError: Invalid time valueon it. EveryDatebranch in the serialization path calledtoISOString()unguarded, so a single invalid Date anywhere in the inspected cache made serialization throw.The important part is where that exception lands.
sync.tsis injected with"world": "MAIN", so it subscribes to the page's realQueryCache.QueryCache.notifyiterates its subscribers without catching, so theRangeErrorunwinds out of the inspected application's own React commit phase and trips its error boundary.In other words: the devtools crash the app they are inspecting, and the app's stack trace points at whichever component happened to unmount, with no hint that an extension is involved. That took a while to track down on our side:
It is also sticky — the bad entry stays in the cache, so every subsequent
notifythrows again until theQueryClientis torn down (for us, a logout/login).Changes
Adds a small guard used by all four
Datesites, rather than fixing only the one that crashed:utils/serialization.ts:62—encodeBigInts(the deep walker; this is the one that crashed)utils/serialization.ts:244—prepareForStringifyutils/serialization.ts:316—serializeToJsLiteralcomponents/TreeView.tsx:269— panel rendering, which would throw for the same reasonInvalid Dates encode as the wire value
"Invalid Date", chosen becausenew Date("Invalid Date")decodes back to an equally invalid Date — so the existingcase "date"decoder needs no change and the round-trip stays faithful. Export emitsnew Date(NaN).Verification
npm run compileandnpm run lintboth clean.npm run prettier:checkreports the same 50 pre-existing files before and after, so I left formatting alone rather than mixing an unrelated reformat into this diff.The repo has no test runner, so I verified out-of-tree against these functions directly, with a
QueryCache-shaped payload holding one invalid and one valid Date:Against unmodified
mainthe first check fails with exactly the reportedRangeError: Invalid time value.Happy to reshape any of this — including dropping the exported helpers for a plain inline guard, or emitting
nullinstead of"Invalid Date"— if you'd prefer a different convention.