diff --git a/packages/query-core/src/__tests__/query.test.tsx b/packages/query-core/src/__tests__/query.test.tsx index 1f76177467..dc87e16fac 100644 --- a/packages/query-core/src/__tests__/query.test.tsx +++ b/packages/query-core/src/__tests__/query.test.tsx @@ -965,4 +965,53 @@ describe('query', () => { await sleep(60) // let it resolve expect(spy).toHaveBeenCalledWith('1 - 2') }) + + it('should have an error status when queryFn data is not serializable', async () => { + const consoleMock = vi.spyOn(console, 'error') + + consoleMock.mockImplementation(() => undefined) + + const key = queryKey() + + const queryFn = vi.fn() + + queryFn.mockImplementation(async () => { + await sleep(10) + + const data: Array<{ + id: number + name: string + link: null | { id: number; name: string; link: unknown } + }> = Array.from({ length: 5 }) + .fill(null) + .map((_, index) => ({ + id: index, + name: `name-${index}`, + link: null, + })) + + if (data[0] && data[1]) { + data[0].link = data[1] + data[1].link = data[0] + } + + return data + }) + + await queryClient.prefetchQuery({ queryKey: key, queryFn }) + + const query = queryCache.find({ queryKey: key })! + + expect(queryFn).toHaveBeenCalledTimes(1) + + expect(consoleMock).toHaveBeenCalledWith( + expect.stringContaining( + 'StructuralSharing requires data to be JSON serializable', + ), + ) + + expect(query.state.status).toBe('error') + + consoleMock.mockRestore() + }) }) diff --git a/packages/query-core/src/query.ts b/packages/query-core/src/query.ts index b51ff6b5be..1160b1acfd 100644 --- a/packages/query-core/src/query.ts +++ b/packages/query-core/src/query.ts @@ -499,7 +499,12 @@ export class Query< return } - this.setData(data) + try { + this.setData(data) + } catch (error) { + onError(error as TError) + return + } // Notify cache callback this.#cache.config.onSuccess?.(data, this as Query) diff --git a/packages/query-core/src/utils.ts b/packages/query-core/src/utils.ts index f84fdcd775..a33671c202 100644 --- a/packages/query-core/src/utils.ts +++ b/packages/query-core/src/utils.ts @@ -353,6 +353,19 @@ export function replaceData< if (typeof options.structuralSharing === 'function') { return options.structuralSharing(prevData, data) as TData } else if (options.structuralSharing !== false) { + if (process.env.NODE_ENV !== 'production') { + try { + JSON.stringify(prevData) + JSON.stringify(data) + } catch (error) { + console.error( + `StructuralSharing requires data to be JSON serializable. To fix this, turn off structuralSharing or return JSON-serializable data from your queryFn. [${options.queryHash}]: ${error}`, + ) + + throw new Error('Data is not serializable') + } + } + // Structurally share data between prev and new data if needed return replaceEqualDeep(prevData, data) }