Skip to content

Conversation

Han5991
Copy link

@Han5991 Han5991 commented Jun 30, 2025

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

  • Bug Fixes

    • Corrected retry-on-mount behavior when using error boundaries with throwOnError, preventing unintended retries and honoring actual error state.
    • Ensured consistent behavior across single and multiple queries, including remounts and infinite stale times.
    • Refined interaction with Suspense and prefetch to avoid unnecessary retries.
  • Tests

    • Expanded coverage for throwOnError (boolean and function), retryOnMount, remount scenarios, and ErrorBoundary rendering to verify correct statuses, errors, and call counts.

options.retryOnMount = false
}
} else if (options.throwOnError) {
if (!errorResetBoundary.isReset() && typeof options.throwOnError !== 'function') {
Copy link
Collaborator

Choose a reason for hiding this comment

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

The function can return true or false so I think we'd need to evaluate it to get the result

Copy link
Author

Choose a reason for hiding this comment

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

Updated to evaluate the throwOnError function result instead of just checking if it's a function. Thanks for the feedback!

@Han5991 Han5991 requested a review from TkDodo June 30, 2025 15:24
@Han5991 Han5991 force-pushed the fix/usequery-retry-on-mount-with-throw-on-error-issue(#9336) branch from f7b90c2 to 9c8516d Compare July 6, 2025 01:31
@Han5991
Copy link
Author

Han5991 commented Jul 7, 2025

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

@Han5991 Han5991 force-pushed the fix/usequery-retry-on-mount-with-throw-on-error-issue(#9336) branch 2 times, most recently from 334eca9 to bcc8460 Compare August 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 Han5991 force-pushed the fix/usequery-retry-on-mount-with-throw-on-error-issue(#9336) branch from bcc8460 to e8e0caa Compare September 20, 2025 04:14
Copy link
Contributor

coderabbitai bot commented Sep 25, 2025

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly summarizes the main fix by specifying that retryOnMount should be allowed when throwOnError is a function, which accurately reflects the changes in errorBoundaryUtils and useQuery hooks.
Linked Issues Check ✅ Passed The changes update ensurePreventErrorBoundaryRetry to distinguish between boolean and function throwOnError values, preserving retryOnMount when throwOnError is a function that returns false, and they add new tests verifying retries on remount and correct queryFn invocation counts for both function and boolean cases, thereby fulfilling the bug reproduction and expected behavior described in issue #9336.
Out of Scope Changes Check ✅ Passed All modifications are centered on refining throwOnError handling and retryOnMount logic across ensurePreventErrorBoundaryRetry, useBaseQuery, useQueries, and corresponding test cases, with no unrelated features or files altered outside the scope of the linked issue’s objectives.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 2
import { 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
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 () => {
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)
})
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>
)
}
// First mount
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)
rendered1.unmount()
const initialFetchCount = fetchCount
// Second mount
const rendered2 = renderWithClient(queryClient, <Component />)
await vi.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
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
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 () => {
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)
})
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>
)
}
// First render
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)
rendered1.unmount()
const initialFetchCount = fetchCount
// Remount in a fresh container
const rendered2 = renderWithClient(queryClient, <Component />)
await vi.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 on lines +56 to +63
const query = client
.getQueryCache()
.get<
TQueryFnData,
TError,
TQueryData,
TQueryKey
>(defaultedOptions.queryHash)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Compute the Query after _experimental_beforeQuery to avoid stale queryHash mismatches.

You fetch the cached query before calling _experimental_beforeQuery, which can mutate options (incl. queryKey/queryHash). This risks passing the wrong query into ensurePreventErrorBoundaryRetry. Inline the cache lookup at the call-site after the hook and drop this precomputed variable.

Apply this diff:

-  const query = client
-    .getQueryCache()
-    .get<
-      TQueryFnData,
-      TError,
-      TQueryData,
-      TQueryKey
-    >(defaultedOptions.queryHash)

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 82 to 84
ensureSuspenseTimers(defaultedOptions)
ensurePreventErrorBoundaryRetry(defaultedOptions, errorResetBoundary)

ensurePreventErrorBoundaryRetry(defaultedOptions, errorResetBoundary, query)
useClearResetErrorBoundary(errorResetBoundary)
Copy link
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.

Copy link

changeset-bot bot commented Sep 26, 2025

⚠️ No Changeset found

Latest commit: cd2dd84

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

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

Sign up for free to 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