diff --git a/.changeset/feat-query-cache-on-error-failure-count.md b/.changeset/feat-query-cache-on-error-failure-count.md new file mode 100644 index 00000000000..cae36735d88 --- /dev/null +++ b/.changeset/feat-query-cache-on-error-failure-count.md @@ -0,0 +1,21 @@ +--- +"@tanstack/query-core": minor +--- + +feat(query-core): pass `failureCount` to `QueryCache` `onError` callback + +The `onError` callback in `QueryCacheConfig` now receives a third argument `failureCount: number` indicating how many retry attempts occurred before the final failure (0 means no retries happened). + +This allows differentiated error handling based on the retry count: + +```ts +new QueryCache({ + onError: (error, query, failureCount) => { + if (failureCount === 0) { + toast.error('Request failed') + } else { + toast.error(`Request failed after ${failureCount} retries`) + } + }, +}) +``` diff --git a/packages/query-core/src/__tests__/queryCache.test.tsx b/packages/query-core/src/__tests__/queryCache.test.tsx index 1238bcf44a9..bf094e78273 100644 --- a/packages/query-core/src/__tests__/queryCache.test.tsx +++ b/packages/query-core/src/__tests__/queryCache.test.tsx @@ -325,12 +325,32 @@ describe('queryCache', () => { }) await vi.advanceTimersByTimeAsync(100) const query = testCache.find({ queryKey: key }) - expect(onError).toHaveBeenCalledWith('error', query) + expect(onError).toHaveBeenCalledWith('error', query, 0) expect(onError).toHaveBeenCalledTimes(1) expect(onSuccess).not.toHaveBeenCalled() expect(onSettled).toHaveBeenCalledTimes(1) expect(onSettled).toHaveBeenCalledWith(undefined, 'error', query) }) + + it('should pass the retry count to onError when retries are exhausted', async () => { + const key = queryKey() + const onError = vi.fn() + const testCache = new QueryCache({ onError }) + const testClient = new QueryClient({ queryCache: testCache }) + testClient.prefetchQuery({ + queryKey: key, + queryFn: () => Promise.reject('error'), + retry: 2, + retryDelay: 10, + }) + // advance past initial attempt + 2 retry delays + await vi.advanceTimersByTimeAsync(10) + await vi.advanceTimersByTimeAsync(10) + await vi.advanceTimersByTimeAsync(10) + const query = testCache.find({ queryKey: key }) + expect(onError).toHaveBeenCalledTimes(1) + expect(onError).toHaveBeenCalledWith('error', query, 2) + }) }) describe('QueryCacheConfig success callbacks', () => { diff --git a/packages/query-core/src/query.ts b/packages/query-core/src/query.ts index 62bc9a16082..349d91eb3fe 100644 --- a/packages/query-core/src/query.ts +++ b/packages/query-core/src/query.ts @@ -532,6 +532,8 @@ export class Query< this.#dispatch({ type: 'fetch', meta: context.fetchOptions?.meta }) } + let retryCount = 0 + // Try to fetch the data this.#retryer = createRetryer({ initialPromise: fetchOptions?.initialPromise as @@ -548,6 +550,7 @@ export class Query< abortController.abort() }, onFail: (failureCount, error) => { + retryCount = failureCount this.#dispatch({ type: 'failed', failureCount, error }) }, onPause: () => { @@ -610,6 +613,7 @@ export class Query< this.#cache.config.onError?.( error as any, this as Query, + retryCount, ) this.#cache.config.onSettled?.( this.state.data, diff --git a/packages/query-core/src/queryCache.ts b/packages/query-core/src/queryCache.ts index dd7123eaac8..fc62de1712a 100644 --- a/packages/query-core/src/queryCache.ts +++ b/packages/query-core/src/queryCache.ts @@ -20,6 +20,7 @@ interface QueryCacheConfig { onError?: ( error: DefaultError, query: Query, + failureCount: number, ) => void onSuccess?: (data: unknown, query: Query) => void onSettled?: (