Skip to content

Commit 18e2744

Browse files
committed
fix(query-core): handle combine throwing in QueriesObserver#notify
When useSuspenseQueries is used with combine and a query transitions to pending/error state (e.g. after resetQueries), the combine function throws because data is undefined despite types narrowing it as defined. The #notify method calls combine as an optimization to check if the combined result changed. If combine throws during this check, we now catch the error and still notify listeners so the framework can re-suspend or show an error boundary. Fixes #10129
1 parent 1047cdc commit 18e2744

2 files changed

Lines changed: 55 additions & 2 deletions

File tree

packages/query-core/src/__tests__/queriesObserver.test.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,4 +630,43 @@ describe('queriesObserver', () => {
630630
{ status: 'success', data: 3 },
631631
])
632632
})
633+
634+
test('should still notify listeners when combine throws after query reset', async () => {
635+
const key1 = queryKey()
636+
const queryFn1 = vi.fn().mockReturnValue({ name: 'test' })
637+
638+
const combine = vi.fn(
639+
(results: Array<QueryObserverResult>) => {
640+
// This simulates a combine function that assumes data is always defined
641+
// (like useSuspenseQueries types suggest)
642+
return results.map((r) => (r.data as { name: string }).name)
643+
},
644+
)
645+
646+
const observer = new QueriesObserver<Array<string>>(
647+
queryClient,
648+
[{ queryKey: key1, queryFn: queryFn1 }],
649+
{ combine },
650+
)
651+
652+
const results: Array<Array<QueryObserverResult>> = []
653+
const unsubscribe = observer.subscribe((result) => {
654+
results.push(result)
655+
})
656+
657+
// Wait for queries to resolve
658+
await vi.advanceTimersByTimeAsync(0)
659+
660+
// Reset the query - this transitions it to pending state
661+
// which should cause combine to throw since data is undefined
662+
queryClient.resetQueries({ queryKey: key1 })
663+
664+
// The listener should still have been notified despite combine throwing
665+
const lastResult = results[results.length - 1]
666+
expect(lastResult).toBeDefined()
667+
expect(lastResult![0]!.status).toBe('pending')
668+
expect(lastResult![0]!.data).toBeUndefined()
669+
670+
unsubscribe()
671+
})
633672
})

packages/query-core/src/queriesObserver.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,9 +296,23 @@ export class QueriesObserver<
296296
if (this.hasListeners()) {
297297
const previousResult = this.#combinedResult
298298
const newTracked = this.#trackResult(this.#result, this.#observerMatches)
299-
const newResult = this.#combineResult(newTracked, this.#options?.combine)
300299

301-
if (previousResult !== newResult) {
300+
let shouldNotify: boolean
301+
try {
302+
const newResult = this.#combineResult(
303+
newTracked,
304+
this.#options?.combine,
305+
)
306+
shouldNotify = previousResult !== newResult
307+
} catch {
308+
// If combine throws (e.g. when used with useSuspenseQueries and
309+
// a query transitions to pending/error state after a reset), we
310+
// still need to notify so the framework can re-suspend or show
311+
// an error boundary.
312+
shouldNotify = true
313+
}
314+
315+
if (shouldNotify) {
302316
notifyManager.batch(() => {
303317
this.listeners.forEach((listener) => {
304318
listener(this.#result)

0 commit comments

Comments
 (0)