From f7ac5f54d5f82ad425dfb6755c02d3e662e469e7 Mon Sep 17 00:00:00 2001 From: S-jooyoung Date: Tue, 13 Oct 2026 15:45:17 +0900 Subject: [PATCH] fix(vue-query): throw falsy errors from useMutation to the error boundary The error watcher in `useMutation` only evaluated `throwOnError` when the error value was truthy, so a mutation that rejected with a falsy value (e.g. `undefined` or `null`) never reached `shouldThrowError` and was not propagated. Watch `state.isError` alongside `state.error` and check `isError` instead, the same approach proposed for react-query in #11311 and preact-query in #11312, and already used by vue-query's `useBaseQuery`. --- .changeset/vue-mutation-falsy-error.md | 5 ++++ .../src/__tests__/useMutation.test.ts | 29 +++++++++++++++++++ packages/vue-query/src/useMutation.ts | 19 +++++------- 3 files changed, 42 insertions(+), 11 deletions(-) create mode 100644 .changeset/vue-mutation-falsy-error.md diff --git a/.changeset/vue-mutation-falsy-error.md b/.changeset/vue-mutation-falsy-error.md new file mode 100644 index 00000000000..6bbed215273 --- /dev/null +++ b/.changeset/vue-mutation-falsy-error.md @@ -0,0 +1,5 @@ +--- +'@tanstack/vue-query': patch +--- + +fix(vue-query): throw falsy errors from `useMutation` to the error boundary diff --git a/packages/vue-query/src/__tests__/useMutation.test.ts b/packages/vue-query/src/__tests__/useMutation.test.ts index 2ac8aad0b86..b1d414ab152 100644 --- a/packages/vue-query/src/__tests__/useMutation.test.ts +++ b/packages/vue-query/src/__tests__/useMutation.test.ts @@ -619,6 +619,35 @@ describe('useMutation', () => { ) }, ) + + // These tests let throwOnError return false on purpose: under vitest, + // throwing a falsy value from the watcher breaks every watcher created + // later in this file, while the throw path is already covered above. + it('should evaluate throwOnError when the mutation rejects with undefined', async () => { + const throwOnError = vi.fn().mockReturnValue(false) + const { mutate } = useMutation({ + mutationFn: () => sleep(10).then(() => Promise.reject(undefined)), + throwOnError, + }) + + mutate() + await vi.advanceTimersByTimeAsync(10) + expect(throwOnError).toHaveBeenCalledTimes(1) + expect(throwOnError).toHaveBeenCalledWith(undefined) + }) + + it('should evaluate throwOnError when the mutation rejects with null', async () => { + const throwOnError = vi.fn().mockReturnValue(false) + const { mutate } = useMutation({ + mutationFn: () => sleep(10).then(() => Promise.reject(null)), + throwOnError, + }) + + mutate() + await vi.advanceTimersByTimeAsync(10) + expect(throwOnError).toHaveBeenCalledTimes(1) + expect(throwOnError).toHaveBeenCalledWith(null) + }) }) describe('optimistic updates', () => { diff --git a/packages/vue-query/src/useMutation.ts b/packages/vue-query/src/useMutation.ts index 799b321ba4d..4ecdd325470 100644 --- a/packages/vue-query/src/useMutation.ts +++ b/packages/vue-query/src/useMutation.ts @@ -291,17 +291,14 @@ export function useMutation< Readonly> > - watch( - () => state.error, - (error) => { - if ( - error && - shouldThrowError(defaultedOptions.value.throwOnError, [error as TError]) - ) { - throw error - } - }, - ) + watch([() => state.isError, () => state.error], ([isError, error]) => { + if ( + isError && + shouldThrowError(defaultedOptions.value.throwOnError, [error as TError]) + ) { + throw error + } + }) return { ...resultRefs,