Skip to content

Conversation

SimonSimCity
Copy link
Contributor

@SimonSimCity SimonSimCity commented Sep 22, 2025

Not to be confused with #9675, which handles a slightly different problem.

By catching the error of onError and onSettled callbacks passed to the useMutation hook and reporting it on a separate execution context, we can ensure that onSettled will be called even if onError throws an error.

For developers, by reporting the error in a separate context, we enable the ability to globally catch those callback errors by using the unhandledRejection event fired on window.

Unchanged remains the error handling of onSuccess, which will (as before) call onError and onSettled.

Just one thing might remain suspicious - and this is still how it has been before: If the mutation function was successful, and the following onSettled throws an error, the onError callbacks are called, and also onSettled - means, onSettled, if it errors, is called twice. I didn't want to go into a deep refactoring here, so I left it this way - the same behaviour as before, just that its second error now raises an unhandledRejection event.

Summary by CodeRabbit

  • Bug Fixes

    • Mutation error handling is now more resilient: failures inside lifecycle callbacks (onSuccess/onError/onSettled) are contained and no longer break mutation flow or produce extra unhandled rejections; original mutation errors are preserved.
  • Tests

    • Added extensive tests for erroneous mutation callbacks, callback ordering, timing, and unhandled-rejection behavior; test harnesses now isolate and clean up global unhandledRejection listeners.
  • Breaking Changes

    • Mutation cache constructor now accepts callback options at creation time (public API change).

Copy link
Contributor

coderabbitai bot commented Sep 22, 2025

Walkthrough

Mutation error-path callbacks are now individually guarded so thrown errors from lifecycle callbacks are converted to rejected promises and do not break the outer mutation error flow; tests expanded for erroneous-callback scenarios and unhandledRejection listeners; MutationCache is imported from package root and used with constructor callback options.

Changes

Cohort / File(s) Summary of Changes
Core mutation error-path
packages/query-core/src/mutation.ts
In the mutation error path, each lifecycle callback invocation (cache.onError, options.onError, cache.onSettled, options.onSettled) is wrapped in its own try/catch; callback-thrown errors are swallowed by forwarding as void Promise.reject(e). Error dispatching and rethrow of the original error occur after these guards.
Tests: expanded erroneous-callback suite
packages/query-core/src/__tests__/mutations.test.tsx
Adds a comprehensive "erroneous mutation callback" test suite exercising error propagation/handling across onMutate/onSuccess/onError/onSettled, global vs local hooks, cross-context unhandledRejection reporting, and ordering/timing of callbacks. Also imports MutationCache from package root and constructs it with callback options.
Public API usage in tests
packages/query-core/...
Tests construct MutationCache with callback options (e.g., onSuccess, onError, onSettled), indicating the constructor now accepts callback options at creation time (public constructor signature usage).
Test harness: unhandledRejection guards (React)
packages/react-query/src/__tests__/useMutation.test.tsx
Two tests now accept ({ onTestFinished }), install a temporary process.on('unhandledRejection', ...) mock listener, and unregister it via onTestFinished to isolate unhandled rejection handling.
Test harness: unhandledRejection guards (Solid)
packages/solid-query/src/__tests__/useMutation.test.tsx
Two tests similarly altered to register and clean up a temporary unhandledRejection listener using onTestFinished, isolating unhandled rejection handling.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User code
  participant M as Mutation.execute
  participant C as MutationCache (cache hooks)
  participant O as Options (per-mutation hooks)

  U->>M: execute() → run mutateFn()
  alt mutateFn throws (E)
    M->>C: cache.onError(E)
    Note right of M: guarded — thrown errors -> void Promise.reject(e)
    M->>O: options.onError(E)
    Note right of M: guarded — thrown errors -> void Promise.reject(e)
    M->>C: cache.onSettled(undefined, E)
    Note right of M: guarded — thrown errors -> void Promise.reject(e)
    M->>O: options.onSettled(undefined, E)
    Note right of M: guarded — thrown errors -> void Promise.reject(e)
    M->>M: dispatch(error, E)
    M-->>U: rethrow original error E
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

I hop through callbacks, soft and spry,
Catching thrown errors as they fly.
onError, onSettled, I net with care,
Let the mutation fail — the flow stays fair.
Tests bloom like clover, tidy and spry. 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The description does not follow the repository’s required PR template: it omits the “## 🎯 Changes”, “## ✅ Checklist”, and “## 🚀 Release Impact” sections and fails to include the specified checkboxes and structured headings. Please update the description to use the provided template by adding a “## 🎯 Changes” section detailing the modifications and motivation, completing the “## ✅ Checklist” with the required items, and filling out the “## 🚀 Release Impact” section with the appropriate checkboxes.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title clearly and concisely states the main change of reporting errors from useMutation callbacks asynchronously, directly reflecting the PR’s purpose and enabling quick understanding of the core update.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

nx-cloud bot commented Sep 22, 2025

View your CI Pipeline Execution ↗ for commit 362d187

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 2m 54s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 1m 22s View ↗

☁️ Nx Cloud last updated this comment at 2025-09-26 09:52:08 UTC

Copy link

pkg-pr-new bot commented Sep 22, 2025

More templates

@tanstack/angular-query-experimental

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

@tanstack/eslint-plugin-query

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

@tanstack/query-async-storage-persister

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

@tanstack/query-broadcast-client-experimental

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

@tanstack/query-core

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

@tanstack/query-devtools

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

@tanstack/query-persist-client-core

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

@tanstack/query-sync-storage-persister

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

@tanstack/react-query

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

@tanstack/react-query-devtools

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

@tanstack/react-query-next-experimental

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

@tanstack/react-query-persist-client

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

@tanstack/solid-query

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

@tanstack/solid-query-devtools

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

@tanstack/solid-query-persist-client

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

@tanstack/svelte-query

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

@tanstack/svelte-query-devtools

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

@tanstack/svelte-query-persist-client

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

@tanstack/vue-query

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

@tanstack/vue-query-devtools

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

commit: 362d187

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: 0

🧹 Nitpick comments (2)
packages/query-core/src/__tests__/mutations.test.tsx (1)

846-849: Process listener cleanup should be more robust.

The afterEach cleanup only removes listeners but doesn't restore any previously registered listeners. This could interfere with other test suites if they also use unhandledRejection listeners.

Consider saving and restoring existing listeners:

+  let originalListeners: Array<(...args: any[]) => void>
+
+  beforeEach(() => {
+    originalListeners = process.listeners('unhandledRejection')
+    process.removeAllListeners('unhandledRejection')
+  })
+
   afterEach(() => {
     process.removeAllListeners('unhandledRejection')
+    originalListeners.forEach(listener => {
+      process.on('unhandledRejection', listener)
+    })
   })
packages/query-core/src/mutation.ts (1)

281-283: Consider adding error context for debugging.

While void Promise.reject(e) correctly reports errors asynchronously, the errors lack context about their origin, which could make debugging challenging in production.

Consider wrapping the error with additional context:

   } catch (e) {
-    void Promise.reject(e)
+    void Promise.reject(Object.assign(
+      e instanceof Error ? e : new Error(String(e)),
+      { source: 'mutationCache.onError', mutationKey: this.options.mutationKey }
+    ))
   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fcd23c9 and 0657280.

📒 Files selected for processing (2)
  • packages/query-core/src/__tests__/mutations.test.tsx (2 hunks)
  • packages/query-core/src/mutation.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/query-core/src/__tests__/mutations.test.tsx (4)
packages/query-core/src/queryClient.ts (1)
  • QueryClient (61-648)
packages/query-core/src/mutationCache.ts (1)
  • MutationCache (93-240)
packages/query-core/src/__tests__/utils.ts (1)
  • executeMutation (13-22)
packages/query-core/src/utils.ts (1)
  • sleep (363-367)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test
  • GitHub Check: Preview
🔇 Additional comments (9)
packages/query-core/src/__tests__/mutations.test.tsx (4)

3-3: Good addition of MutationCache import for test scenarios.

The direct import of MutationCache enables the new test suite to exercise cache-level callbacks, which is essential for testing the enhanced error handling paths.


851-907: Well-structured test for global onSuccess error propagation.

This test correctly validates that errors thrown by global onSuccess callbacks trigger the onError callback path, which is a key improvement in the error handling logic.


959-1026: Excellent test coverage for the double-invocation of onSettled.

The test properly validates the documented behavior where onSettled can be invoked twice when an error occurs - once after success and once after the error handling. The unhandled rejection verification at line 1022-1023 confirms that the second error is properly reported asynchronously.


1087-1161: Comprehensive test for multiple callback errors.

This test thoroughly validates that all callback errors are properly captured as unhandled rejections (lines 1153-1160), while the original mutation error is still propagated correctly (line 1151). The verification of all four unhandled rejections ensures no errors are silently swallowed.

packages/query-core/src/mutation.ts (5)

271-283: Critical improvement: Isolated error handling for cache onError callback.

The try-catch wrapper prevents the cache's onError callback from disrupting the error handling flow. The void Promise.reject(e) pattern correctly schedules the error for asynchronous reporting without blocking execution.


285-294: Good isolation of options onError callback.

Each callback is now independently wrapped, ensuring that failures in one don't prevent others from executing. This aligns with the PR objective of ensuring onSettled is called even if onError throws.


296-320: Proper implementation of the onSettled double-invocation pattern.

The separate try-catch blocks for cache and options onSettled callbacks ensure both are invoked even if either throws. This maintains the existing behavior where onSettled may be called twice (once after success, once after error) while preventing callback errors from disrupting the flow.


322-323: Correct preservation of original error propagation.

The dispatch and throw statements correctly maintain the original mutation error flow, ensuring backward compatibility while the new try-catch blocks handle callback errors separately.


282-282: Verified: void Promise.reject(e) triggers Node unhandledRejection; no browser handler present — confirm desired behavior

  • Node: tests register process.on('unhandledRejection', ...) and assert it is called; occurrences in packages/query-core/src/mutation.ts at lines 282, 293, 307, 319.
  • Browser: no window.addEventListener('unhandledrejection') found in repo; browsers emit 'unhandledrejection' for unhandled Promise rejections — add a browser-level handler if you need to capture/report these errors.

@SimonSimCity SimonSimCity force-pushed the report-use-mutation-callback-errors-async branch from 0657280 to d3755da Compare September 22, 2025 17:03
@SimonSimCity SimonSimCity force-pushed the report-use-mutation-callback-errors-async branch from d3755da to 9e26948 Compare September 22, 2025 17:05
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: 0

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

1067-1074: Avoid global listener teardown; remove only the handler you add.

removeAllListeners('unhandledRejection') wipes unrelated listeners and can cause cross-test flakiness. Store the handler and off it in onTestFinished.

Apply:

-it('should go to error state if onError callback errors', async ({ onTestFinished }) => {
-  onTestFinished(() => {
-    process.removeAllListeners('unhandledRejection')
-  })
-  process.on('unhandledRejection', vi.fn())
+it('should go to error state if onError callback errors', async ({ onTestFinished }) => {
+  const handler = vi.fn()
+  process.on('unhandledRejection', handler)
+  onTestFinished(() => {
+    process.off('unhandledRejection', handler)
+  })

1111-1118: Same targeted cleanup here.

Mirror the scoped listener removal pattern to avoid nuking global listeners.

Apply:

-it('should go to error state if onSettled callback errors', async ({ onTestFinished }) => {
-  onTestFinished(() => {
-    process.removeAllListeners('unhandledRejection')
-  })
-  process.on('unhandledRejection', vi.fn())
+it('should go to error state if onSettled callback errors', async ({ onTestFinished }) => {
+  const handler = vi.fn()
+  process.on('unhandledRejection', handler)
+  onTestFinished(() => {
+    process.off('unhandledRejection', handler)
+  })
packages/query-core/src/mutation.ts (1)

281-321: Asynchronous error reporting looks right; consider deduping the pattern.

The void Promise.reject(e) approach meets the goal (surfaces unhandledrejection without perturbing control flow). Repeating it four times hurts readability—extract a tiny helper.

Apply:

+// Report callback-thrown errors on a separate microtask so they surface as unhandled rejections.
+const reportUnhandled = (e: unknown) => {
+  // eslint-disable-next-line @typescript-eslint/no-floating-promises
+  Promise.reject(e)
+}
...
-} catch (e) {
-  void Promise.reject(e)
+} catch (e) {
+  reportUnhandled(e)
}
...
-} catch (e) {
-  void Promise.reject(e)
+} catch (e) {
+  reportUnhandled(e)
}
...
-} catch (e) {
-  void Promise.reject(e)
+} catch (e) {
+  reportUnhandled(e)
}
...
-} catch (e) {
-  void Promise.reject(e)
+} catch (e) {
+  reportUnhandled(e)
}

Note: keep the helper file-local (not exported).

packages/query-core/src/__tests__/mutations.test.tsx (3)

846-849: Don’t nuke all unhandledRejection listeners.

Use targeted removal instead of removeAllListeners to prevent interference with other suites.

Apply:

-describe('erroneous mutation callback', () => {
-  afterEach(() => {
-    process.removeAllListeners('unhandledRejection')
-  })
+describe('erroneous mutation callback', () => {
+  let addedHandlers: Array<(...args: any[]) => void> = []
+  afterEach(() => {
+    for (const h of addedHandlers) process.off('unhandledRejection', h)
+    addedHandlers = []
+  })

And when adding listeners inside tests, push them to addedHandlers.


962-979: Minor safety/readability: declare results before using in cache config.

The current closure works because invocation happens later, but hoisting results avoids TDZ pitfalls if this test ever changes.

Apply:

-  queryClient = new QueryClient({
+  const results: Array<string> = []
+  queryClient = new QueryClient({
     mutationCache: new MutationCache({
       onSettled: async () => {
         results.push('global-onSettled')
         await sleep(10)
         throw newMutationError
       },
     }),
   })
   queryClient.mount()
-
-  const unhandledRejectionFn = vi.fn()
+  const unhandledRejectionFn = vi.fn()
   process.on('unhandledRejection', (error) => unhandledRejectionFn(error))
-
-  const key = queryKey()
-  const results: Array<string> = []
+  const key = queryKey()

1087-1090: Test name nit: grammar.

“it are reported” → “they are reported”.

Apply:

-test('errors by onError and consecutive onSettled callbacks are transferred to different execution context where it are reported', async () => {
+test('errors by onError and consecutive onSettled callbacks are transferred to a different execution context where they are reported', async () => {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0657280 and 9e26948.

📒 Files selected for processing (3)
  • packages/query-core/src/__tests__/mutations.test.tsx (2 hunks)
  • packages/query-core/src/mutation.ts (2 hunks)
  • packages/react-query/src/__tests__/useMutation.test.tsx (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/query-core/src/__tests__/mutations.test.tsx (4)
packages/query-core/src/queryClient.ts (1)
  • QueryClient (61-648)
packages/query-core/src/mutationCache.ts (1)
  • MutationCache (93-240)
packages/query-core/src/__tests__/utils.ts (1)
  • executeMutation (13-22)
packages/query-core/src/utils.ts (1)
  • sleep (363-367)
🔇 Additional comments (2)
packages/query-core/src/mutation.ts (1)

322-323: Preserves original error semantics.

Dispatching the mutation error and rethrowing the original error aligns with prior behavior and the PR intent.

If SSR environments in your ecosystem treat unhandled rejections as hard failures, call this out in release notes to avoid surprises.

packages/query-core/src/__tests__/mutations.test.tsx (1)

3-3: Import from package root is good.

Aligns tests with the public API surface.

@SimonSimCity SimonSimCity force-pushed the report-use-mutation-callback-errors-async branch from 9e26948 to 66c5e21 Compare September 22, 2025 17:57
Copy link

codecov bot commented Sep 22, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 60.70%. Comparing base (cd29063) to head (362d187).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##             main    #9676       +/-   ##
===========================================
+ Coverage   46.38%   60.70%   +14.31%     
===========================================
  Files         214      143       -71     
  Lines        8488     5733     -2755     
  Branches     1920     1536      -384     
===========================================
- Hits         3937     3480      -457     
+ Misses       4108     1953     -2155     
+ Partials      443      300      -143     
Components Coverage Δ
@tanstack/angular-query-experimental 93.85% <ø> (ø)
@tanstack/eslint-plugin-query ∅ <ø> (∅)
@tanstack/query-async-storage-persister 43.85% <ø> (ø)
@tanstack/query-broadcast-client-experimental 24.39% <ø> (ø)
@tanstack/query-codemods ∅ <ø> (∅)
@tanstack/query-core 97.49% <100.00%> (+<0.01%) ⬆️
@tanstack/query-devtools 3.48% <ø> (ø)
@tanstack/query-persist-client-core 79.60% <ø> (ø)
@tanstack/query-sync-storage-persister 84.61% <ø> (ø)
@tanstack/query-test-utils ∅ <ø> (∅)
@tanstack/react-query 96.00% <ø> (ø)
@tanstack/react-query-devtools 10.00% <ø> (ø)
@tanstack/react-query-next-experimental ∅ <ø> (∅)
@tanstack/react-query-persist-client 100.00% <ø> (ø)
@tanstack/solid-query 78.06% <ø> (ø)
@tanstack/solid-query-devtools ∅ <ø> (∅)
@tanstack/solid-query-persist-client 100.00% <ø> (ø)
@tanstack/svelte-query 87.58% <ø> (ø)
@tanstack/svelte-query-devtools ∅ <ø> (∅)
@tanstack/svelte-query-persist-client 100.00% <ø> (ø)
@tanstack/vue-query 71.10% <ø> (ø)
@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.

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: 0

🧹 Nitpick comments (5)
packages/solid-query/src/__tests__/useMutation.test.tsx (1)

1121-1128: Prefer removing only the listener you add (avoid removeAllListeners).

Using removeAllListeners risks nuking unrelated handlers added by other tests. Capture the listener and remove just that one.

Apply this diff:

-  onTestFinished(() => {
-    process.removeAllListeners('unhandledRejection')
-  })
-  process.on('unhandledRejection', vi.fn())
+  const listener = vi.fn()
+  process.on('unhandledRejection', listener)
+  onTestFinished(() => {
+    process.off('unhandledRejection', listener)
+  })
packages/query-core/src/mutation.ts (1)

281-324: Asynchronous reporting approach looks solid; consider de-duplicating the “void Promise.reject(e)” pattern.

Current behavior correctly surfaces callback-thrown errors via unhandledrejection while preserving the original mutation error. For readability and future maintenance, extract a helper.

Apply this diff:

-      } catch (e) {
-        void Promise.reject(e)
-      }
+      } catch (e) {
+        reportUnhandledRejection(e)
+      }
@@
-      } catch (e) {
-        void Promise.reject(e)
-      }
+      } catch (e) {
+        reportUnhandledRejection(e)
+      }
@@
-      } catch (e) {
-        void Promise.reject(e)
-      }
+      } catch (e) {
+        reportUnhandledRejection(e)
+      }
@@
-      } catch (e) {
-        void Promise.reject(e)
-      }
+      } catch (e) {
+        reportUnhandledRejection(e)
+      }

Add this helper in the module (top-level, near other utils):

function reportUnhandledRejection(e: unknown): void {
  // Surface callback errors asynchronously without breaking control flow
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
  Promise.reject(e)
}
packages/query-core/src/__tests__/mutations.test.tsx (3)

847-849: Avoid removeAllListeners in test cleanup.

Limit cleanup to the listener(s) added by each test to prevent side effects across tests in the same process.

Example per-test pattern:

const unhandledRejectionFn = vi.fn()
process.on('unhandledRejection', unhandledRejectionFn)
// ...test...
process.off('unhandledRejection', unhandledRejectionFn)

962-972: TDZ footgun: “results” is referenced in the MutationCache callback before its declaration.

While it works because the callback runs later, referencing a const declared later relies on timing and can confuse readers. Declare results before constructing QueryClient.

Apply this diff inside the test:

-  queryClient = new QueryClient({
-    mutationCache: new MutationCache({
-      onSettled: async () => {
-        results.push('global-onSettled')
-        await sleep(10)
-        throw newMutationError
-      },
-    }),
-  })
-  queryClient.mount()
-
-  const key = queryKey()
-  const results: Array<string> = []
+  const results: Array<string> = []
+
+  queryClient = new QueryClient({
+    mutationCache: new MutationCache({
+      onSettled: async () => {
+        results.push('global-onSettled')
+        await sleep(10)
+        throw newMutationError
+      },
+    }),
+  })
+  queryClient.mount()
+
+  const key = queryKey()

Also applies to: 977-977


1087-1087: Test name grammar nit.

Consider: “errors by onError and consecutive onSettled callbacks are transferred to a different execution context where they are reported”.

-it('errors by onError and consecutive onSettled callbacks are transferred to different execution context where it are reported', async () => {
+it('errors by onError and consecutive onSettled callbacks are transferred to a different execution context where they are reported', async () => {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9e26948 and 66c5e21.

📒 Files selected for processing (4)
  • packages/query-core/src/__tests__/mutations.test.tsx (2 hunks)
  • packages/query-core/src/mutation.ts (2 hunks)
  • packages/react-query/src/__tests__/useMutation.test.tsx (2 hunks)
  • packages/solid-query/src/__tests__/useMutation.test.tsx (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/react-query/src/tests/useMutation.test.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
packages/query-core/src/__tests__/mutations.test.tsx (4)
packages/query-core/src/queryClient.ts (1)
  • QueryClient (61-648)
packages/query-core/src/mutationCache.ts (1)
  • MutationCache (93-240)
packages/query-core/src/__tests__/utils.ts (1)
  • executeMutation (13-22)
packages/query-core/src/utils.ts (1)
  • sleep (363-367)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Preview
  • GitHub Check: Test
🔇 Additional comments (2)
packages/solid-query/src/__tests__/useMutation.test.tsx (1)

1169-1176: Same cleanup nit as above: remove the specific listener, not all.

-  onTestFinished(() => {
-    process.removeAllListeners('unhandledRejection')
-  })
-  process.on('unhandledRejection', vi.fn())
+  const listener = vi.fn()
+  process.on('unhandledRejection', listener)
+  onTestFinished(() => {
+    process.off('unhandledRejection', listener)
+  })
packages/query-core/src/__tests__/mutations.test.tsx (1)

3-3: Import change LGTM.

Importing MutationCache from the package root aligns tests with public API usage.

Copy link

changeset-bot bot commented Sep 26, 2025

⚠️ No Changeset found

Latest commit: 362d187

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.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

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

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: 0

🧹 Nitpick comments (4)
packages/query-core/src/mutation.ts (4)

281-283: Surface callback errors asynchronously — consider a small helper to deduplicate and document intent

void Promise.reject(e) correctly routes thrown callback errors to global unhandledrejection without breaking the control flow. To avoid repetition and make intent explicit, extract a helper and use it here.

Apply within this range:

-      } catch (e) {
-        void Promise.reject(e)
-      }
+      } catch (e) {
+        reportUnhandled(e)
+      }

Add once in this file (outside the shown range), e.g. near the top-level utilities:

function reportUnhandled(e: unknown): void {
  // Surface callback errors to global `unhandledrejection` listeners without disrupting mutation flow
  void Promise.reject(e)
}

285-294: Use the same helper for options.onError wrapper

Repeat the helper to keep behavior consistent and reduce duplication.

-      try {
+      try {
         await this.options.onError?.(
           error as TError,
           variables,
           this.state.context,
           mutationFnContext,
         )
-      } catch (e) {
-        void Promise.reject(e)
-      }
+      } catch (e) {
+        reportUnhandled(e)
+      }

296-308: Apply helper for cache onSettled wrapper

Same rationale; keep all error-path wrappers uniform.

-      try {
+      try {
         // Notify cache callback
         await this.#mutationCache.config.onSettled?.(
           undefined,
           error as any,
           this.state.variables,
           this.state.context,
           this as Mutation<unknown, unknown, unknown, unknown>,
           mutationFnContext,
         )
-      } catch (e) {
-        void Promise.reject(e)
-      }
+      } catch (e) {
+        reportUnhandled(e)
+      }

310-319: Apply helper for options.onSettled wrapper

Consistent handling and clearer intent.

-      try {
+      try {
         await this.options.onSettled?.(
           undefined,
           error as TError,
           variables,
           this.state.context,
           mutationFnContext,
         )
-      } catch (e) {
-        void Promise.reject(e)
-      }
+      } catch (e) {
+        reportUnhandled(e)
+      }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ba4e95a and 362d187.

📒 Files selected for processing (1)
  • packages/query-core/src/mutation.ts (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Preview
  • GitHub Check: Test
🔇 Additional comments (1)
packages/query-core/src/mutation.ts (1)

322-323: Confirm error dispatch ordering matches prior behavior
Dispatching the error after executing all error-path callbacks alters observable event timing; verify this matches previous versions to avoid regressions in consumers relying on specific ordering.

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.

2 participants