Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
331 changes: 330 additions & 1 deletion packages/query-core/src/__tests__/mutations.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { queryKey, sleep } from '@tanstack/query-test-utils'
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { QueryClient } from '..'
import { MutationCache, QueryClient } from '..'
import { MutationObserver } from '../mutationObserver'
import { executeMutation } from './utils'
import type { MutationState } from '../mutation'
Expand Down Expand Up @@ -842,4 +842,333 @@ describe('mutations', () => {
expect(mutationError).toEqual(newMutationError)
})
})

describe('erroneous mutation callback', () => {
test('error by global onSuccess triggers onError callback', async () => {
const newMutationError = new Error('mutation-error')

queryClient = new QueryClient({
mutationCache: new MutationCache({
onSuccess: () => {
throw newMutationError
},
}),
})
queryClient.mount()

const key = queryKey()
const results: Array<string> = []

let mutationError: Error | undefined
executeMutation(
queryClient,
{
mutationKey: key,
mutationFn: () => Promise.resolve('success'),
onMutate: async () => {
results.push('onMutate-async')
await sleep(10)
return { backup: 'async-data' }
},
onSuccess: async () => {
results.push('onSuccess-async-start')
await sleep(10)
throw newMutationError
},
onError: async () => {
results.push('onError-async-start')
await sleep(10)
results.push('onError-async-end')
},
onSettled: () => {
results.push('onSettled-promise')
return Promise.resolve('also-ignored') // Promise<string> (should be ignored)
},
},
'vars',
).catch((error) => {
mutationError = error
})

await vi.advanceTimersByTimeAsync(30)

expect(results).toEqual([
'onMutate-async',
'onError-async-start',
'onError-async-end',
'onSettled-promise',
])

expect(mutationError).toEqual(newMutationError)
})

test('error by mutations onSuccess triggers onError callback', async () => {
const key = queryKey()
const results: Array<string> = []

const newMutationError = new Error('mutation-error')

let mutationError: Error | undefined
executeMutation(
queryClient,
{
mutationKey: key,
mutationFn: () => Promise.resolve('success'),
onMutate: async () => {
results.push('onMutate-async')
await sleep(10)
return { backup: 'async-data' }
},
onSuccess: async () => {
results.push('onSuccess-async-start')
await sleep(10)
throw newMutationError
},
onError: async () => {
results.push('onError-async-start')
await sleep(10)
results.push('onError-async-end')
},
onSettled: () => {
results.push('onSettled-promise')
return Promise.resolve('also-ignored') // Promise<string> (should be ignored)
},
},
'vars',
).catch((error) => {
mutationError = error
})

await vi.advanceTimersByTimeAsync(30)

expect(results).toEqual([
'onMutate-async',
'onSuccess-async-start',
'onError-async-start',
'onError-async-end',
'onSettled-promise',
])

expect(mutationError).toEqual(newMutationError)
})

test('error by global onSettled triggers onError callback, calling global onSettled callback twice', async ({
onTestFinished,
}) => {
const newMutationError = new Error('mutation-error')

queryClient = new QueryClient({
mutationCache: new MutationCache({
onSettled: async () => {
results.push('global-onSettled')
await sleep(10)
throw newMutationError
},
}),
})
queryClient.mount()

const unhandledRejectionFn = vi.fn()
process.on('unhandledRejection', (error) => unhandledRejectionFn(error))
onTestFinished(() => {
process.off('unhandledRejection', unhandledRejectionFn)
})

const key = queryKey()
const results: Array<string> = []

let mutationError: Error | undefined
executeMutation(
queryClient,
{
mutationKey: key,
mutationFn: () => Promise.resolve('success'),
onMutate: async () => {
results.push('onMutate-async')
await sleep(10)
return { backup: 'async-data' }
},
onSuccess: async () => {
results.push('onSuccess-async-start')
await sleep(10)
results.push('onSuccess-async-end')
},
onError: async () => {
results.push('onError-async-start')
await sleep(10)
results.push('onError-async-end')
},
onSettled: () => {
results.push('local-onSettled')
},
},
'vars',
).catch((error) => {
mutationError = error
})

await vi.advanceTimersByTimeAsync(50)

expect(results).toEqual([
'onMutate-async',
'onSuccess-async-start',
'onSuccess-async-end',
'global-onSettled',
'onError-async-start',
'onError-async-end',
'global-onSettled',
'local-onSettled',
])

expect(unhandledRejectionFn).toHaveBeenCalledTimes(1)
expect(unhandledRejectionFn).toHaveBeenNthCalledWith(1, newMutationError)

expect(mutationError).toEqual(newMutationError)
})

test('error by mutations onSettled triggers onError callback, calling both onSettled callbacks twice', async ({
onTestFinished,
}) => {
const unhandledRejectionFn = vi.fn()
process.on('unhandledRejection', (error) => unhandledRejectionFn(error))
onTestFinished(() => {
process.off('unhandledRejection', unhandledRejectionFn)
})

const key = queryKey()
const results: Array<string> = []

const newMutationError = new Error('mutation-error')

let mutationError: Error | undefined
executeMutation(
queryClient,
{
mutationKey: key,
mutationFn: () => Promise.resolve('success'),
onMutate: async () => {
results.push('onMutate-async')
await sleep(10)
return { backup: 'async-data' }
},
onSuccess: async () => {
results.push('onSuccess-async-start')
await sleep(10)
results.push('onSuccess-async-end')
},
onError: async () => {
results.push('onError-async-start')
await sleep(10)
results.push('onError-async-end')
},
onSettled: async () => {
results.push('onSettled-async-promise')
await sleep(10)
throw newMutationError
},
},
'vars',
).catch((error) => {
mutationError = error
})

await vi.advanceTimersByTimeAsync(50)

expect(results).toEqual([
'onMutate-async',
'onSuccess-async-start',
'onSuccess-async-end',
'onSettled-async-promise',
'onError-async-start',
'onError-async-end',
'onSettled-async-promise',
])

expect(unhandledRejectionFn).toHaveBeenCalledTimes(1)
expect(unhandledRejectionFn).toHaveBeenNthCalledWith(1, newMutationError)

expect(mutationError).toEqual(newMutationError)
})

test('errors by onError and consecutive onSettled callbacks are transferred to different execution context where it are reported', async ({
onTestFinished,
}) => {
const unhandledRejectionFn = vi.fn()
process.on('unhandledRejection', (error) => unhandledRejectionFn(error))
onTestFinished(() => {
process.off('unhandledRejection', unhandledRejectionFn)
})

const globalErrorError = new Error('global-error-error')
const globalSettledError = new Error('global-settled-error')

queryClient = new QueryClient({
mutationCache: new MutationCache({
onError: () => {
throw globalErrorError
},
onSettled: () => {
throw globalSettledError
},
}),
})
queryClient.mount()

const key = queryKey()
const results: Array<string> = []

const newMutationError = new Error('mutation-error')
const newErrorError = new Error('error-error')
const newSettledError = new Error('settled-error')

let mutationError: Error | undefined
executeMutation(
queryClient,
{
mutationKey: key,
mutationFn: () => Promise.resolve('success'),
onMutate: async () => {
results.push('onMutate-async')
await sleep(10)
throw newMutationError
},
onSuccess: () => {
results.push('onSuccess-async-start')
},
onError: async () => {
results.push('onError-async-start')
await sleep(10)
throw newErrorError
},
onSettled: async () => {
results.push('onSettled-promise')
await sleep(10)
throw newSettledError
},
},
'vars',
).catch((error) => {
mutationError = error
})

await vi.advanceTimersByTimeAsync(30)

expect(results).toEqual([
'onMutate-async',
'onError-async-start',
'onSettled-promise',
])

expect(mutationError).toEqual(newMutationError)

expect(unhandledRejectionFn).toHaveBeenCalledTimes(4)
expect(unhandledRejectionFn).toHaveBeenNthCalledWith(1, globalErrorError)
expect(unhandledRejectionFn).toHaveBeenNthCalledWith(2, newErrorError)
expect(unhandledRejectionFn).toHaveBeenNthCalledWith(
3,
globalSettledError,
)
expect(unhandledRejectionFn).toHaveBeenNthCalledWith(4, newSettledError)
})
})
})
20 changes: 17 additions & 3 deletions packages/query-core/src/mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,14 +278,22 @@ export class Mutation<
this as Mutation<unknown, unknown, unknown, unknown>,
mutationFnContext,
)
} catch (e) {
void Promise.reject(e)
}

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

try {
// Notify cache callback
await this.#mutationCache.config.onSettled?.(
undefined,
Expand All @@ -295,18 +303,24 @@ export class Mutation<
this as Mutation<unknown, unknown, unknown, unknown>,
mutationFnContext,
)
} catch (e) {
void Promise.reject(e)
}

try {
await this.options.onSettled?.(
undefined,
error as TError,
variables,
this.state.context,
mutationFnContext,
)
throw error
} finally {
this.#dispatch({ type: 'error', error: error as TError })
} catch (e) {
void Promise.reject(e)
}

this.#dispatch({ type: 'error', error: error as TError })
throw error
} finally {
this.#mutationCache.runNext(this)
}
Expand Down
Loading
Loading