Skip to content

Commit 3ee1ae4

Browse files
SimonSimCitylachlancollinsTkDodo
authored
fix(core): report errors of mutation callbacks asynchronously (#9675)
* Ensure errors of mutation callbacks are reported asynchronously * Fixed accidental removal of vitests unhandledRejection handler --------- Co-authored-by: Lachlan Collins <[email protected]> Co-authored-by: Dominik Dorfmeister <[email protected]>
1 parent 84564f1 commit 3ee1ae4

File tree

2 files changed

+135
-26
lines changed

2 files changed

+135
-26
lines changed

packages/query-core/src/__tests__/mutationObserver.test.tsx

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,4 +383,97 @@ describe('mutationObserver', () => {
383383

384384
unsubscribe()
385385
})
386+
387+
describe('erroneous mutation callback', () => {
388+
test('onSuccess and onSettled is transferred to different execution context where it is reported', async ({
389+
onTestFinished,
390+
}) => {
391+
const unhandledRejectionFn = vi.fn()
392+
process.on('unhandledRejection', (error) => unhandledRejectionFn(error))
393+
onTestFinished(() => {
394+
process.off('unhandledRejection', unhandledRejectionFn)
395+
})
396+
397+
const onSuccessError = new Error('onSuccess-error')
398+
const onSuccess = vi.fn(() => {
399+
throw onSuccessError
400+
})
401+
const onSettledError = new Error('onSettled-error')
402+
const onSettled = vi.fn(() => {
403+
throw onSettledError
404+
})
405+
406+
const mutationObserver = new MutationObserver(queryClient, {
407+
mutationFn: (text: string) => Promise.resolve(text.toUpperCase()),
408+
})
409+
410+
const subscriptionHandler = vi.fn()
411+
const unsubscribe = mutationObserver.subscribe(subscriptionHandler)
412+
413+
mutationObserver.mutate('success', {
414+
onSuccess,
415+
onSettled,
416+
})
417+
418+
await vi.advanceTimersByTimeAsync(0)
419+
420+
expect(onSuccess).toHaveBeenCalledTimes(1)
421+
expect(onSettled).toHaveBeenCalledTimes(1)
422+
423+
expect(unhandledRejectionFn).toHaveBeenCalledTimes(2)
424+
expect(unhandledRejectionFn).toHaveBeenNthCalledWith(1, onSuccessError)
425+
expect(unhandledRejectionFn).toHaveBeenNthCalledWith(2, onSettledError)
426+
427+
expect(subscriptionHandler).toHaveBeenCalledTimes(2)
428+
429+
unsubscribe()
430+
})
431+
432+
test('onError and onSettled is transferred to different execution context where it is reported', async ({
433+
onTestFinished,
434+
}) => {
435+
const unhandledRejectionFn = vi.fn()
436+
process.on('unhandledRejection', (error) => unhandledRejectionFn(error))
437+
onTestFinished(() => {
438+
process.off('unhandledRejection', unhandledRejectionFn)
439+
})
440+
441+
const onErrorError = new Error('onError-error')
442+
const onError = vi.fn(() => {
443+
throw onErrorError
444+
})
445+
const onSettledError = new Error('onSettled-error')
446+
const onSettled = vi.fn(() => {
447+
throw onSettledError
448+
})
449+
450+
const error = new Error('error')
451+
const mutationObserver = new MutationObserver(queryClient, {
452+
mutationFn: (_: string) => Promise.reject(error),
453+
})
454+
455+
const subscriptionHandler = vi.fn()
456+
const unsubscribe = mutationObserver.subscribe(subscriptionHandler)
457+
458+
mutationObserver
459+
.mutate('error', {
460+
onError,
461+
onSettled,
462+
})
463+
.catch(() => {})
464+
465+
await vi.advanceTimersByTimeAsync(0)
466+
467+
expect(onError).toHaveBeenCalledTimes(1)
468+
expect(onSettled).toHaveBeenCalledTimes(1)
469+
470+
expect(unhandledRejectionFn).toHaveBeenCalledTimes(2)
471+
expect(unhandledRejectionFn).toHaveBeenNthCalledWith(1, onErrorError)
472+
expect(unhandledRejectionFn).toHaveBeenNthCalledWith(2, onSettledError)
473+
474+
expect(subscriptionHandler).toHaveBeenCalledTimes(2)
475+
476+
unsubscribe()
477+
})
478+
})
386479
})

packages/query-core/src/mutationObserver.ts

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -172,33 +172,49 @@ export class MutationObserver<
172172
} satisfies MutationFunctionContext
173173

174174
if (action?.type === 'success') {
175-
this.#mutateOptions.onSuccess?.(
176-
action.data,
177-
variables,
178-
onMutateResult,
179-
context,
180-
)
181-
this.#mutateOptions.onSettled?.(
182-
action.data,
183-
null,
184-
variables,
185-
onMutateResult,
186-
context,
187-
)
175+
try {
176+
this.#mutateOptions.onSuccess?.(
177+
action.data,
178+
variables,
179+
onMutateResult,
180+
context,
181+
)
182+
} catch (e) {
183+
void Promise.reject(e)
184+
}
185+
try {
186+
this.#mutateOptions.onSettled?.(
187+
action.data,
188+
null,
189+
variables,
190+
onMutateResult,
191+
context,
192+
)
193+
} catch (e) {
194+
void Promise.reject(e)
195+
}
188196
} else if (action?.type === 'error') {
189-
this.#mutateOptions.onError?.(
190-
action.error,
191-
variables,
192-
onMutateResult,
193-
context,
194-
)
195-
this.#mutateOptions.onSettled?.(
196-
undefined,
197-
action.error,
198-
variables,
199-
onMutateResult,
200-
context,
201-
)
197+
try {
198+
this.#mutateOptions.onError?.(
199+
action.error,
200+
variables,
201+
onMutateResult,
202+
context,
203+
)
204+
} catch (e) {
205+
void Promise.reject(e)
206+
}
207+
try {
208+
this.#mutateOptions.onSettled?.(
209+
undefined,
210+
action.error,
211+
variables,
212+
onMutateResult,
213+
context,
214+
)
215+
} catch (e) {
216+
void Promise.reject(e)
217+
}
202218
}
203219
}
204220

0 commit comments

Comments
 (0)