Skip to content

fix(react-query): allow retryOnMount when throwOnError is function (#9336) - #9338

Merged
TkDodo merged 19 commits into
TanStack:mainfrom
Han5991:fix/usequery-retry-on-mount-with-throw-on-error-issue(#9336)
Dec 30, 2025
Merged

fix(react-query): allow retryOnMount when throwOnError is function (#9336)#9338
TkDodo merged 19 commits into
TanStack:mainfrom
Han5991:fix/usequery-retry-on-mount-with-throw-on-error-issue(#9336)

Conversation

@Han5991

@Han5991Han5991 commented Jun 30, 2025

Copy link
Copy Markdown
Contributor

Fixed#9336

Summary

  • Fixed throwOnError retry logic to properly handle function vs boolean values
  • When throwOnError is a function, allow retryOnMount to proceed even when error boundary hasn't been reset
  • Maintains existing prevention behavior for boolean throwOnError values
  • Added comprehensive test coverage for the retry behavior

Summary by CodeRabbit

  • Tests

    • Expanded test coverage for retry and error-boundary behaviors, including mount/unmount scenarios and error-state-aware retries.
  • Bug Fixes

    • Improved error-boundary integration to make retry-on-mount decisions more accurate when error handling is conditional.
  • Chores

    • Small bundle size adjustments and added a changeset documenting a patch release addressing retry behavior with conditional error handling.

✏️ Tip: You can customize this high-level summary in your review settings.

Comment threadpackages/react-query/src/errorBoundaryUtils.ts Outdated
@Han5991
Han5991 requested a review from TkDodoJune 30, 2025 15:24
@Han5991
Han5991force-pushed the fix/usequery-retry-on-mount-with-throw-on-error-issue(#9336) branch from f7b90c2 to 9c8516dCompareJuly 6, 2025 01:31
@Han5991

Copy link
Copy Markdown
ContributorAuthor

@TkDodo Hi! Simple fix for retryOnMount when throwOnError is function.
Would appreciate a review when you have time! 🙏

@Han5991
Han5991force-pushed the fix/usequery-retry-on-mount-with-throw-on-error-issue(#9336) branch 2 times, most recently from 334eca9 to bcc8460CompareAugust 13, 2025 14:20
…cases
When throwOnError is a function, allow retryOnMount to proceed even when error boundary hasn't reset,
while maintaining the prevention behavior for boolean throwOnError values.
Refine behavior to handle function-type throwOnError, allowing retries when appropriate. Ensure boolean throwOnError values still prevent retries when the error boundary isn't reset.
This commit introduces tests to verify the behavior of the `throwOnError` callback in scenarios where `retryOnMount` is enabled. It ensures proper handling of retries based on the error state and the `throwOnError` function's return value. These tests improve the reliability and coverage of error handling logic in `useQuery`.
- Pass query object to ensurePreventErrorBoundaryRetry for accurate state checking
- Preserve query deduplication behavior while fixing throwOnError function handling
- Fixes issue where throwOnError function couldn't access query error state
@Han5991
Han5991force-pushed the fix/usequery-retry-on-mount-with-throw-on-error-issue(#9336) branch from bcc8460 to e8e0caaCompareSeptember 20, 2025 04:14
@coderabbitai

coderabbitaiBot commented Sep 25, 2025

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

The PR changes retry-on-mount behavior by evaluating throwOnError at runtime for a specific query. ensurePreventErrorBoundaryRetry accepts an optional query and now only suppresses retryOnMount when appropriate (evaluating throwOnError when it's a function). Hooks pass the query from the cache; tests added.

Changes

Cohort / File(s)Summary
Tests
packages/react-query/src/__tests__/useQuery.test.tsx
Added tests covering retry behavior with throwOnError (function and boolean), ErrorBoundary-wrapped components, mount/unmount/remount cycles, retryOnMount, and staleTime: Infinity.
Error boundary behavior
packages/react-query/src/errorBoundaryUtils.ts
ensurePreventErrorBoundaryRetry signature gains optional query. Logic split: suspense/prefetch handled early; throwOnError evaluated (if function) against query.state.error before disabling retryOnMount.
Hook callers
packages/react-query/src/useBaseQuery.ts, packages/react-query/src/useQueries.ts
Hooks now fetch the Query from the cache and pass it into ensurePreventErrorBoundaryRetry. useQueries loop variable renamed to avoid shadowing.
CI / size limits
.size-limit.json
Increased size limits for "react full" and "react minimal".
Release notes
.changeset/long-ties-rescue.md
Added changeset noting fix: allow retryOnMount when throwOnError is a function that returns false.

Sequence Diagram(s)

sequenceDiagram
autonumber
participant Hook as useBaseQuery / useQueries
participant Cache as QueryCache
participant Query as Query (query.state)
participant EBUtils as ensurePreventErrorBoundaryRetry
participant ErrorReset as ErrorResetBoundary
rect rgba(200,230,255,0.4)
Hook->>Cache: get(queryHash)
Cache-->>Hook: Query?
Hook->>EBUtils: ensurePreventErrorBoundaryRetry(options, ErrorReset, Query)
end
rect rgba(230,255,220,0.4)
EBUtils->>EBUtils: if options.suspense || experimental_prefetchInRender -> return
EBUtils->>Query: read error state (if provided)
alt throwOnError is function
EBUtils->>EBUtils: shouldThrowError(query.state.error, throwOnError)
EBUtils-->>Hook: maybe set retryOnMount = false
else throwOnError truthy
EBUtils-->>Hook: set retryOnMount = false
end
end
Note over Hook,ErrorReset: Hook proceeds with query lifecycle honoring modified retryOnMount
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 I peeked in caches, soft and bright,

throwOnError whispered wrong or right.
Now mounts retry when errors say "no",
Remounts hop back — the queries go!
A rabbit cheers for fetches in flight.

Pre-merge checks

❌ Failed checks (1 inconclusive)
Check nameStatusExplanationResolution
Description check❓ InconclusiveThe PR description lacks the required template structure including the 🎯 Changes section details and ✅ Checklist items, though it summarizes the changes made.Complete the PR description using the required template format, including the Changes section and all checklist items, to ensure consistency with repository standards.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly and concisely summarizes the main fix: allowing retryOnMount when throwOnError is a function, directly addressing issue #9336.
Linked Issues check✅ PassedThe code changes directly address issue #9336 by distinguishing function vs boolean throwOnError values and allowing retryOnMount for function-based throwOnError, with comprehensive test coverage added.
Out of Scope Changes check✅ PassedThe PR includes a minor bundle size update in .size-limit.json (from 12.10 to 12.13 kB for 'react full') which is a natural consequence of the code changes and not out-of-scope.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 75c1e69 and 9fd84a6.

📒 Files selected for processing (1)
  • .changeset/long-ties-rescue.md
✅ Files skipped from review due to trivial changes (1)
  • .changeset/long-ties-rescue.md

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
packages/react-query/src/__tests__/useQuery.test.tsx (2)

6785-6962: Optional: remove console.log noise in tests.

The added tests log on each fetch attempt. Consider removing to keep CI output clean.

- console.log(`Fetching... (attempt ${fetchCount})`)

1-10: If you prefer waitFor, import from @testing-library/react (optional alternative).

If you want to keep waitFor semantics instead of timer flushes, add the import and replace vi.advanceTimersByTimeAsync calls accordingly.

Additional change outside the selected ranges:

// at line 2import{act,fireEvent,render,waitFor}from'@testing-library/react'
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cd29063 and 4b795be.

📒 Files selected for processing (4)
  • packages/react-query/src/__tests__/useQuery.test.tsx (7 hunks)
  • packages/react-query/src/errorBoundaryUtils.ts (1 hunks)
  • packages/react-query/src/useBaseQuery.ts (2 hunks)
  • packages/react-query/src/useQueries.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
packages/react-query/src/errorBoundaryUtils.ts (2)
packages/query-core/src/index.ts (2)
  • Query (53-53)
  • shouldThrowError (37-37)
packages/query-core/src/utils.ts (1)
  • shouldThrowError (446-456)
packages/react-query/src/useBaseQuery.ts (2)
packages/query-core/src/queryObserver.ts (1)
  • query (702-719)
packages/react-query/src/errorBoundaryUtils.ts (1)
  • ensurePreventErrorBoundaryRetry (13-47)
packages/react-query/src/__tests__/useQuery.test.tsx (1)
packages/react-query/src/__tests__/utils.tsx (1)
  • renderWithClient (9-23)
packages/react-query/src/useQueries.ts (2)
packages/react-query/src/suspense.ts (1)
  • ensureSuspenseTimers (21-47)
packages/react-query/src/errorBoundaryUtils.ts (1)
  • ensurePreventErrorBoundaryRetry (13-47)
🔇 Additional comments (2)
packages/react-query/src/errorBoundaryUtils.ts (1)

28-46: Correctly evaluate function-valued throwOnError and gate retryOnMount.

This implements the intended behavior: only disable retryOnMount when the error boundary isn't reset AND throwOnError is:

  • a function that returns true for the current error, or
  • a boolean true.

Looks good.

packages/react-query/src/useQueries.ts (1)

245-249: Pass the cached Query into ensurePreventErrorBoundaryRetry (good).

Aligns multi-query flow with the updated errorBoundary handling. No issues.

Comment on lines +6785 to +6835
it('should retry on mount when throwOnError returns false', async () => {
const key = queryKey()
let fetchCount = 0
const queryFn = vi.fn().mockImplementation(() => {
fetchCount++
console.log(`Fetching... (attempt ${fetchCount})`)
return Promise.reject(new Error('Simulated 500 error'))
})

function Component() {
const { status, error } = useQuery({
queryKey: key,
queryFn,
throwOnError: () => false,
retryOnMount: true,
staleTime: Infinity,
retry: false,
})

return (
<div>
<div data-testid="status">{status}</div>
{error && <div data-testid="error">{error.message}</div>}
</div>
)
}

const { unmount, getByTestId } = renderWithClient(
queryClient,
<Component />,
)

await vi.waitFor(() =>
expect(getByTestId('status')).toHaveTextContent('error'),
)
expect(getByTestId('error')).toHaveTextContent('Simulated 500 error')
expect(fetchCount).toBe(1)

unmount()

const initialFetchCount = fetchCount

renderWithClient(queryClient, <Component />)

await vi.waitFor(() =>
expect(getByTestId('status')).toHaveTextContent('error'),
)

expect(fetchCount).toBe(initialFetchCount + 1)
expect(queryFn).toHaveBeenCalledTimes(2)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix test flakiness: use fake-timer flush and don’t reuse getByTestId after unmount.

  • vi.waitFor is not a vitest API; flush with vi.advanceTimersByTimeAsync(0) instead (fake timers are enabled).
  • Don’t reuse queries from an unmounted render; capture a new render result.

Apply this diff:

- const { unmount, getByTestId } = renderWithClient(- queryClient,- <Component />,- )-- await vi.waitFor(() =>- expect(getByTestId('status')).toHaveTextContent('error'),- )- expect(getByTestId('error')).toHaveTextContent('Simulated 500 error')+ const rendered1 = renderWithClient(queryClient, <Component />)+ await vi.advanceTimersByTimeAsync(0)+ expect(rendered1.getByTestId('status')).toHaveTextContent('error')+ expect(rendered1.getByTestId('error')).toHaveTextContent(+ 'Simulated 500 error',+ )- expect(fetchCount).toBe(1)-- unmount()+ expect(fetchCount).toBe(1)+ rendered1.unmount()
@@
- renderWithClient(queryClient, <Component />)-- await vi.waitFor(() =>- expect(getByTestId('status')).toHaveTextContent('error'),- )+ const rendered2 = renderWithClient(queryClient, <Component />)+ await vi.advanceTimersByTimeAsync(0)+ expect(rendered2.getByTestId('status')).toHaveTextContent('error')
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('should retry on mount when throwOnError returns false',async()=>{
constkey=queryKey()
letfetchCount=0
constqueryFn=vi.fn().mockImplementation(()=>{
fetchCount++
console.log(`Fetching... (attempt ${fetchCount})`)
returnPromise.reject(newError('Simulated 500 error'))
})
functionComponent(){
const{ status, error }=useQuery({
queryKey: key,
queryFn,
throwOnError: ()=>false,
retryOnMount: true,
staleTime: Infinity,
retry: false,
})
return(
<div>
<divdata-testid="status">{status}</div>
{error&&<divdata-testid="error">{error.message}</div>}
</div>
)
}
const{ unmount, getByTestId }=renderWithClient(
queryClient,
<Component/>,
)
awaitvi.waitFor(()=>
expect(getByTestId('status')).toHaveTextContent('error'),
)
expect(getByTestId('error')).toHaveTextContent('Simulated 500 error')
expect(fetchCount).toBe(1)
unmount()
constinitialFetchCount=fetchCount
renderWithClient(queryClient,<Component/>)
awaitvi.waitFor(()=>
expect(getByTestId('status')).toHaveTextContent('error'),
)
expect(fetchCount).toBe(initialFetchCount+1)
expect(queryFn).toHaveBeenCalledTimes(2)
})
it('should retry on mount when throwOnError returns false',async()=>{
constkey=queryKey()
letfetchCount=0
constqueryFn=vi.fn().mockImplementation(()=>{
fetchCount++
console.log(`Fetching... (attempt ${fetchCount})`)
returnPromise.reject(newError('Simulated 500 error'))
})
functionComponent(){
const{ status, error }=useQuery({
queryKey: key,
queryFn,
throwOnError: ()=>false,
retryOnMount: true,
staleTime: Infinity,
retry: false,
})
return(
<div>
<divdata-testid="status">{status}</div>
{error&&<divdata-testid="error">{error.message}</div>}
</div>
)
}
// First mount
constrendered1=renderWithClient(queryClient,<Component/>)
awaitvi.advanceTimersByTimeAsync(0)
expect(rendered1.getByTestId('status')).toHaveTextContent('error')
expect(
rendered1.getByTestId('error')
).toHaveTextContent('Simulated 500 error')
expect(fetchCount).toBe(1)
rendered1.unmount()
constinitialFetchCount=fetchCount
// Second mount
constrendered2=renderWithClient(queryClient,<Component/>)
awaitvi.advanceTimersByTimeAsync(0)
expect(rendered2.getByTestId('status')).toHaveTextContent('error')
expect(fetchCount).toBe(initialFetchCount+1)
expect(queryFn).toHaveBeenCalledTimes(2)
})
🤖 Prompt for AI Agents
packages/react-query/src/__tests__/useQuery.test.tsx around lines 6785 to 6835:
the test is flaky because it uses vi.waitFor (not a vitest API with fake timers)
and reuses getByTestId from a component after unmount; replace the vi.waitFor
flush with vi.advanceTimersByTimeAsync(0) to advance fake timers and ensure task
queue is flushed, and when re-rendering after unmount capture the new render
result (assign the return of renderWithClient to a new variable) and call
getByTestId on that new result instead of reusing the previous one so assertions
target a mounted DOM.

Comment on lines +6837 to +6909
it('should not retry on mount when throwOnError function returns true', async () => {
const key = queryKey()
let fetchCount = 0
const queryFn = vi.fn().mockImplementation(() => {
fetchCount++
console.log(`Fetching... (attempt ${fetchCount})`)
return Promise.reject(new Error('Simulated 500 error'))
})

function Component() {
const { status, error } = useQuery({
queryKey: key,
queryFn,
throwOnError: () => true,
retryOnMount: true,
staleTime: Infinity,
retry: false,
})

return (
<div>
<div data-testid="status">{status}</div>
{error && <div data-testid="error">{error.message}</div>}
</div>
)
}

const { unmount, getByTestId } = renderWithClient(
queryClient,
<ErrorBoundary
fallbackRender={({ error }) => (
<div>
<div data-testid="status">error</div>
<div data-testid="error">{error?.message}</div>
</div>
)}
>
<Component />
</ErrorBoundary>,
)

await vi.waitFor(() =>
expect(getByTestId('status')).toHaveTextContent('error'),
)
expect(getByTestId('error')).toHaveTextContent('Simulated 500 error')
expect(fetchCount).toBe(1)

unmount()

const initialFetchCount = fetchCount

renderWithClient(
queryClient,
<ErrorBoundary
fallbackRender={({ error }) => (
<div>
<div data-testid="status">error</div>
<div data-testid="error">{error?.message}</div>
</div>
)}
>
<Component />
</ErrorBoundary>,
)

await vi.waitFor(() =>
expect(getByTestId('status')).toHaveTextContent('error'),
)

// Should not retry because throwOnError returns true
expect(fetchCount).toBe(initialFetchCount)
expect(queryFn).toHaveBeenCalledTimes(1)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Same test issues: replace vi.waitFor and avoid stale container after unmount.

Mirror the fixes from the previous test; also keep assertions via the newly captured render result.

- const { unmount, getByTestId } = renderWithClient(- queryClient,- <ErrorBoundary- fallbackRender={({ error }) => (- <div>- <div data-testid="status">error</div>- <div data-testid="error">{error?.message}</div>- </div>- )}- >- <Component />- </ErrorBoundary>,- )-- await vi.waitFor(() =>- expect(getByTestId('status')).toHaveTextContent('error'),- )- expect(getByTestId('error')).toHaveTextContent('Simulated 500 error')+ const rendered1 = renderWithClient(+ queryClient,+ <ErrorBoundary+ fallbackRender={({ error }) => (+ <div>+ <div data-testid="status">error</div>+ <div data-testid="error">{error?.message}</div>+ </div>+ )}+ >+ <Component />+ </ErrorBoundary>,+ )+ await vi.advanceTimersByTimeAsync(0)+ expect(rendered1.getByTestId('status')).toHaveTextContent('error')+ expect(rendered1.getByTestId('error')).toHaveTextContent('Simulated 500 error')
@@
- unmount()+ rendered1.unmount()
@@
- renderWithClient(- queryClient,- <ErrorBoundary- fallbackRender={({ error }) => (- <div>- <div data-testid="status">error</div>- <div data-testid="error">{error?.message}</div>- </div>- )}- >- <Component />- </ErrorBoundary>,- )-- await vi.waitFor(() =>- expect(getByTestId('status')).toHaveTextContent('error'),- )+ const rendered2 = renderWithClient(+ queryClient,+ <ErrorBoundary+ fallbackRender={({ error }) => (+ <div>+ <div data-testid="status">error</div>+ <div data-testid="error">{error?.message}</div>+ </div>+ )}+ >+ <Component />+ </ErrorBoundary>,+ )+ await vi.advanceTimersByTimeAsync(0)+ expect(rendered2.getByTestId('status')).toHaveTextContent('error')
🤖 Prompt for AI Agents
In packages/react-query/src/__tests__/useQuery.test.tsx around lines 6837 to
6909, the test still uses vi.waitFor and reads from a stale container after
unmount; replace vi.waitFor with the testing-library async waitFor helper and
capture the render result for the second render to avoid querying the old
container. Specifically, import/use waitFor (not vi.waitFor), store the result
of the second renderWithClient call in a variable (e.g., const { getByTestId } =
renderWithClient(...)) and use that getByTestId for the post-unmount assertions,
and update the async waits to await waitFor(() => expect(...)) so the test no
longer queries a stale container.

Comment on lines +6911 to +6962
it('should handle throwOnError function based on actual error state', async () => {
const key = queryKey()
let fetchCount = 0
const queryFn = vi.fn().mockImplementation(() => {
fetchCount++
console.log(`Fetching... (attempt ${fetchCount})`)
return Promise.reject(new Error('Simulated 500 error'))
})

function Component() {
const { status, error } = useQuery({
queryKey: key,
queryFn,
throwOnError: (error) => error.message.includes('404'),
retryOnMount: true,
staleTime: Infinity,
retry: false,
})

return (
<div>
<div data-testid="status">{status}</div>
{error && <div data-testid="error">{error.message}</div>}
</div>
)
}

const { unmount, getByTestId } = renderWithClient(
queryClient,
<Component />,
)

await vi.waitFor(() =>
expect(getByTestId('status')).toHaveTextContent('error'),
)
expect(getByTestId('error')).toHaveTextContent('Simulated 500 error')
expect(fetchCount).toBe(1)

unmount()

const initialFetchCount = fetchCount

renderWithClient(queryClient, <Component />)

await vi.waitFor(() =>
expect(getByTestId('status')).toHaveTextContent('error'),
)

// Should retry because throwOnError returns false (500 error doesn't include '404')
expect(fetchCount).toBe(initialFetchCount + 1)
expect(queryFn).toHaveBeenCalledTimes(2)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Same fixes for the 404/500 predicate case.

Use timer flush and distinct render containers after remount.

- const { unmount, getByTestId } = renderWithClient(- queryClient,- <Component />,- )-- await vi.waitFor(() =>- expect(getByTestId('status')).toHaveTextContent('error'),- )- expect(getByTestId('error')).toHaveTextContent('Simulated 500 error')+ const rendered1 = renderWithClient(queryClient, <Component />)+ await vi.advanceTimersByTimeAsync(0)+ expect(rendered1.getByTestId('status')).toHaveTextContent('error')+ expect(rendered1.getByTestId('error')).toHaveTextContent('Simulated 500 error')
@@
- unmount()+ rendered1.unmount()
@@
- renderWithClient(queryClient, <Component />)-- await vi.waitFor(() =>- expect(getByTestId('status')).toHaveTextContent('error'),- )+ const rendered2 = renderWithClient(queryClient, <Component />)+ await vi.advanceTimersByTimeAsync(0)+ expect(rendered2.getByTestId('status')).toHaveTextContent('error')
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('should handle throwOnError function based on actual error state',async()=>{
constkey=queryKey()
letfetchCount=0
constqueryFn=vi.fn().mockImplementation(()=>{
fetchCount++
console.log(`Fetching... (attempt ${fetchCount})`)
returnPromise.reject(newError('Simulated 500 error'))
})
functionComponent(){
const{ status, error }=useQuery({
queryKey: key,
queryFn,
throwOnError: (error)=>error.message.includes('404'),
retryOnMount: true,
staleTime: Infinity,
retry: false,
})
return(
<div>
<divdata-testid="status">{status}</div>
{error&&<divdata-testid="error">{error.message}</div>}
</div>
)
}
const{ unmount, getByTestId }=renderWithClient(
queryClient,
<Component/>,
)
awaitvi.waitFor(()=>
expect(getByTestId('status')).toHaveTextContent('error'),
)
expect(getByTestId('error')).toHaveTextContent('Simulated 500 error')
expect(fetchCount).toBe(1)
unmount()
constinitialFetchCount=fetchCount
renderWithClient(queryClient,<Component/>)
awaitvi.waitFor(()=>
expect(getByTestId('status')).toHaveTextContent('error'),
)
// Should retry because throwOnError returns false (500 error doesn't include '404')
expect(fetchCount).toBe(initialFetchCount+1)
expect(queryFn).toHaveBeenCalledTimes(2)
})
it('should handle throwOnError function based on actual error state',async()=>{
constkey=queryKey()
letfetchCount=0
constqueryFn=vi.fn().mockImplementation(()=>{
fetchCount++
console.log(`Fetching... (attempt ${fetchCount})`)
returnPromise.reject(newError('Simulated 500 error'))
})
functionComponent(){
const{ status, error }=useQuery({
queryKey: key,
queryFn,
throwOnError: (error)=>error.message.includes('404'),
retryOnMount: true,
staleTime: Infinity,
retry: false,
})
return(
<div>
<divdata-testid="status">{status}</div>
{error&&<divdata-testid="error">{error.message}</div>}
</div>
)
}
// First render
constrendered1=renderWithClient(queryClient,<Component/>)
awaitvi.advanceTimersByTimeAsync(0)
expect(rendered1.getByTestId('status')).toHaveTextContent('error')
expect(rendered1.getByTestId('error')).toHaveTextContent('Simulated 500 error')
expect(fetchCount).toBe(1)
rendered1.unmount()
constinitialFetchCount=fetchCount
// Remount in a fresh container
constrendered2=renderWithClient(queryClient,<Component/>)
awaitvi.advanceTimersByTimeAsync(0)
expect(rendered2.getByTestId('status')).toHaveTextContent('error')
// Should retry because throwOnError returns false (500 error doesn't include '404')
expect(fetchCount).toBe(initialFetchCount+1)
expect(queryFn).toHaveBeenCalledTimes(2)
})

Comment threadpackages/react-query/src/useBaseQuery.ts
Comment on lines 82 to 84
ensureSuspenseTimers(defaultedOptions)
ensurePreventErrorBoundaryRetry(defaultedOptions, errorResetBoundary)

ensurePreventErrorBoundaryRetry(defaultedOptions, errorResetBoundary, query)
useClearResetErrorBoundary(errorResetBoundary)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Use a fresh cache lookup when calling ensurePreventErrorBoundaryRetry.

Inline the query read here so it reflects any changes done by _experimental_beforeQuery.

- ensurePreventErrorBoundaryRetry(defaultedOptions, errorResetBoundary, query)+ ensurePreventErrorBoundaryRetry(+ defaultedOptions,+ errorResetBoundary,+ client+ .getQueryCache()+ .get<TQueryFnData, TError, TQueryData, TQueryKey>(+ defaultedOptions.queryHash,+ ),+ )
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ensureSuspenseTimers(defaultedOptions)
ensurePreventErrorBoundaryRetry(defaultedOptions,errorResetBoundary)
ensurePreventErrorBoundaryRetry(defaultedOptions,errorResetBoundary,query)
useClearResetErrorBoundary(errorResetBoundary)
ensureSuspenseTimers(defaultedOptions)
ensurePreventErrorBoundaryRetry(
defaultedOptions,
errorResetBoundary,
client
.getQueryCache()
.get<TQueryFnData,TError,TQueryData,TQueryKey>(
defaultedOptions.queryHash,
),
)
useClearResetErrorBoundary(errorResetBoundary)
🤖 Prompt for AI Agents
In packages/react-query/src/useBaseQuery.ts around lines 82 to 84, inline a
fresh query cache lookup when calling ensurePreventErrorBoundaryRetry instead of
passing the stale "query" variable so changes from _experimental_beforeQuery are
reflected; replace the current call with one that reads the up-to-date query
from the queryCache (e.g. perform a direct cache lookup/find here) and pass that
freshQuery into ensurePreventErrorBoundaryRetry, keeping the surrounding
ensureSuspenseTimers and useClearResetErrorBoundary calls unchanged.

@changeset-bot

changeset-botBot commented Sep 26, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9fd84a6

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Han5991and others added 4 commits October 7, 2025 17:07
…#9338)
- Replace vi.waitFor with vi.advanceTimersByTimeAsync in tests
- Use separate render results to avoid stale DOM references
- Inline fresh query lookup in ensurePreventErrorBoundaryRetry after _experimental_beforeQuery
Comment threadpackages/react-query/src/useBaseQuery.ts
Comment threadpackages/react-query/src/errorBoundaryUtils.ts
Comment threadpackages/react-query/src/errorBoundaryUtils.ts Outdated
@nx-cloud

nx-cloudBot commented Dec 28, 2025

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 9fd84a6

CommandStatusDurationResult
nx affected --targets=test:sherif,test:knip,tes...✅ Succeeded53sView ↗
nx run-many --target=build --exclude=examples/*...✅ Succeeded1sView ↗

☁️ Nx Cloud last updated this comment at 2025-12-30 14:28:14 UTC

autofix-ciBotand others added 2 commits December 28, 2025 20:04
- move query retrieval after `_experimental_beforeQuery` in `useBaseQuery`
- refactor `ensurePreventErrorBoundaryRetry` for better clarity
- make `query` parameter required in `ensurePreventErrorBoundaryRetry`
@Han5991
Han5991 requested a review from TkDodoDecember 28, 2025 23:45
@Han5991

Han5991 commented Dec 28, 2025

Copy link
Copy Markdown
ContributorAuthor

Hi @TkDodo, thank you for the review! I've applied your suggestions:

  • Moved the query retrieval after _experimental_beforeQuery in useBaseQuery.
  • Simplified ensurePreventErrorBoundaryRetry logic and made the query parameter required as requested.
  • Updated useQueries to safely handle the required query parameter.
    All tests are passing. Ready for another look!

Comment threadpackages/react-query/src/errorBoundaryUtils.ts Outdated
@Han5991
Han5991 requested a review from TkDodoDecember 30, 2025 00:58
@pkg-pr-new

pkg-pr-newBot commented Dec 30, 2025

Copy link
Copy Markdown
More templates

@tanstack/angular-query-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-experimental@9338

@tanstack/eslint-plugin-query

npm i https://pkg.pr.new/@tanstack/eslint-plugin-query@9338

@tanstack/query-async-storage-persister

npm i https://pkg.pr.new/@tanstack/query-async-storage-persister@9338

@tanstack/query-broadcast-client-experimental

npm i https://pkg.pr.new/@tanstack/query-broadcast-client-experimental@9338

@tanstack/query-core

npm i https://pkg.pr.new/@tanstack/query-core@9338

@tanstack/query-devtools

npm i https://pkg.pr.new/@tanstack/query-devtools@9338

@tanstack/query-persist-client-core

npm i https://pkg.pr.new/@tanstack/query-persist-client-core@9338

@tanstack/query-sync-storage-persister

npm i https://pkg.pr.new/@tanstack/query-sync-storage-persister@9338

@tanstack/react-query

npm i https://pkg.pr.new/@tanstack/react-query@9338

@tanstack/react-query-devtools

npm i https://pkg.pr.new/@tanstack/react-query-devtools@9338

@tanstack/react-query-next-experimental

npm i https://pkg.pr.new/@tanstack/react-query-next-experimental@9338

@tanstack/react-query-persist-client

npm i https://pkg.pr.new/@tanstack/react-query-persist-client@9338

@tanstack/solid-query

npm i https://pkg.pr.new/@tanstack/solid-query@9338

@tanstack/solid-query-devtools

npm i https://pkg.pr.new/@tanstack/solid-query-devtools@9338

@tanstack/solid-query-persist-client

npm i https://pkg.pr.new/@tanstack/solid-query-persist-client@9338

@tanstack/svelte-query

npm i https://pkg.pr.new/@tanstack/svelte-query@9338

@tanstack/svelte-query-devtools

npm i https://pkg.pr.new/@tanstack/svelte-query-devtools@9338

@tanstack/svelte-query-persist-client

npm i https://pkg.pr.new/@tanstack/svelte-query-persist-client@9338

@tanstack/vue-query

npm i https://pkg.pr.new/@tanstack/vue-query@9338

@tanstack/vue-query-devtools

npm i https://pkg.pr.new/@tanstack/vue-query-devtools@9338

commit: 9fd84a6

Comment threadpackages/react-query/src/useBaseQuery.ts Outdated
Comment threadpackages/react-query/src/useQueries.ts Outdated
@codecov

codecovBot commented Dec 30, 2025

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.12%. Comparing base (0a1e3e0) to head (9fd84a6).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@ Coverage Diff @@## main #9338 +/- ##
===========================================
+ Coverage 45.79% 84.12% +38.32% 
===========================================
Files 200 26 -174 Lines 8520 378 -8142 Branches 1975 111 -1864 ===========================================
- Hits 3902 318 -3584 + Misses 4158 51 -4107 + Partials 460 9 -451 
ComponentsCoverage Δ
@tanstack/angular-query-experimental∅ <ø> (∅)
@tanstack/eslint-plugin-query∅ <ø> (∅)
@tanstack/query-async-storage-persister∅ <ø> (∅)
@tanstack/query-broadcast-client-experimental∅ <ø> (∅)
@tanstack/query-codemods∅ <ø> (∅)
@tanstack/query-core∅ <ø> (∅)
@tanstack/query-devtools∅ <ø> (∅)
@tanstack/query-persist-client-core∅ <ø> (∅)
@tanstack/query-sync-storage-persister∅ <ø> (∅)
@tanstack/query-test-utils∅ <ø> (∅)
@tanstack/react-query96.40% <100.00%> (+0.05%)⬆️
@tanstack/react-query-devtools9.25% <ø> (ø)
@tanstack/react-query-next-experimental∅ <ø> (∅)
@tanstack/react-query-persist-client100.00% <ø> (ø)
@tanstack/solid-query∅ <ø> (∅)
@tanstack/solid-query-devtools∅ <ø> (∅)
@tanstack/solid-query-persist-client∅ <ø> (∅)
@tanstack/svelte-query∅ <ø> (∅)
@tanstack/svelte-query-devtools∅ <ø> (∅)
@tanstack/svelte-query-persist-client∅ <ø> (∅)
@tanstack/vue-query∅ <ø> (∅)
@tanstack/vue-query-devtools∅ <ø> (∅)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@TkDodo
TkDodo merged commit 4be3ad7 into TanStack:mainDec 30, 2025
9 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Dec 30, 2025
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR has been released!

Thank you for your contribution!

TkDodo added a commit that referenced this pull request Aug 20, 2026
…11166)
Ports #9338 to preact-query: ensurePreventErrorBoundaryRetry now
receives the cached query and evaluates a function-form throwOnError
against the actual error before setting retryOnMount = false, instead
of treating any function as truthy. Includes the regression tests
from #9338, two of which fail on current main.
Fixes#11165
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Dominik Dorfmeister 🔮 <office@dorfmeister.cc>
@github-actionsgithub-actionsBot mentioned this pull request Aug 20, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

useQuery does not perform a retry on mount when the function provided to throwOnError returns false, even if retryOnMount: true is set

2 participants

@Han5991@TkDodo