Skip to content

Commit e907f89

Browse files
SimonSimCitylachlancollinsTkDodo
authored
fix(core): report errors of useMutation callbacks asynchronously (#9676)
* Ensure errors of useMutation callbacks onError and onSettled are reported asynchronously * Fixed accidental removal of vitests unhandledRejection handler * chore: size-limit --------- Co-authored-by: Lachlan Collins <1667261+lachlancollins@users.noreply.github.com> Co-authored-by: Dominik Dorfmeister <office@dorfmeister.cc>
1 parent 3ee1ae4 commit e907f89

File tree

5 files changed

+384
-9
lines changed

5 files changed

+384
-9
lines changed

.size-limit.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
{
99
"name": "react minimal",
1010
"path": "packages/react-query/build/modern/index.js",
11-
"limit": "9.11 kB",
11+
"limit": "9.12 kB",
1212
"import": "{ useQuery, QueryClient, QueryClientProvider }",
1313
"ignore": ["react", "react-dom"]
1414
}

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

Lines changed: 330 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { queryKey, sleep } from '@tanstack/query-test-utils'
22
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
3-
import { QueryClient } from '..'
3+
import { MutationCache, QueryClient } from '..'
44
import { MutationObserver } from '../mutationObserver'
55
import { executeMutation } from './utils'
66
import type { MutationState } from '../mutation'
@@ -842,4 +842,333 @@ describe('mutations', () => {
842842
expect(mutationError).toEqual(newMutationError)
843843
})
844844
})
845+
846+
describe('erroneous mutation callback', () => {
847+
test('error by global onSuccess triggers onError callback', async () => {
848+
const newMutationError = new Error('mutation-error')
849+
850+
queryClient = new QueryClient({
851+
mutationCache: new MutationCache({
852+
onSuccess: () => {
853+
throw newMutationError
854+
},
855+
}),
856+
})
857+
queryClient.mount()
858+
859+
const key = queryKey()
860+
const results: Array<string> = []
861+
862+
let mutationError: Error | undefined
863+
executeMutation(
864+
queryClient,
865+
{
866+
mutationKey: key,
867+
mutationFn: () => Promise.resolve('success'),
868+
onMutate: async () => {
869+
results.push('onMutate-async')
870+
await sleep(10)
871+
return { backup: 'async-data' }
872+
},
873+
onSuccess: async () => {
874+
results.push('onSuccess-async-start')
875+
await sleep(10)
876+
throw newMutationError
877+
},
878+
onError: async () => {
879+
results.push('onError-async-start')
880+
await sleep(10)
881+
results.push('onError-async-end')
882+
},
883+
onSettled: () => {
884+
results.push('onSettled-promise')
885+
return Promise.resolve('also-ignored') // Promise<string> (should be ignored)
886+
},
887+
},
888+
'vars',
889+
).catch((error) => {
890+
mutationError = error
891+
})
892+
893+
await vi.advanceTimersByTimeAsync(30)
894+
895+
expect(results).toEqual([
896+
'onMutate-async',
897+
'onError-async-start',
898+
'onError-async-end',
899+
'onSettled-promise',
900+
])
901+
902+
expect(mutationError).toEqual(newMutationError)
903+
})
904+
905+
test('error by mutations onSuccess triggers onError callback', async () => {
906+
const key = queryKey()
907+
const results: Array<string> = []
908+
909+
const newMutationError = new Error('mutation-error')
910+
911+
let mutationError: Error | undefined
912+
executeMutation(
913+
queryClient,
914+
{
915+
mutationKey: key,
916+
mutationFn: () => Promise.resolve('success'),
917+
onMutate: async () => {
918+
results.push('onMutate-async')
919+
await sleep(10)
920+
return { backup: 'async-data' }
921+
},
922+
onSuccess: async () => {
923+
results.push('onSuccess-async-start')
924+
await sleep(10)
925+
throw newMutationError
926+
},
927+
onError: async () => {
928+
results.push('onError-async-start')
929+
await sleep(10)
930+
results.push('onError-async-end')
931+
},
932+
onSettled: () => {
933+
results.push('onSettled-promise')
934+
return Promise.resolve('also-ignored') // Promise<string> (should be ignored)
935+
},
936+
},
937+
'vars',
938+
).catch((error) => {
939+
mutationError = error
940+
})
941+
942+
await vi.advanceTimersByTimeAsync(30)
943+
944+
expect(results).toEqual([
945+
'onMutate-async',
946+
'onSuccess-async-start',
947+
'onError-async-start',
948+
'onError-async-end',
949+
'onSettled-promise',
950+
])
951+
952+
expect(mutationError).toEqual(newMutationError)
953+
})
954+
955+
test('error by global onSettled triggers onError callback, calling global onSettled callback twice', async ({
956+
onTestFinished,
957+
}) => {
958+
const newMutationError = new Error('mutation-error')
959+
960+
queryClient = new QueryClient({
961+
mutationCache: new MutationCache({
962+
onSettled: async () => {
963+
results.push('global-onSettled')
964+
await sleep(10)
965+
throw newMutationError
966+
},
967+
}),
968+
})
969+
queryClient.mount()
970+
971+
const unhandledRejectionFn = vi.fn()
972+
process.on('unhandledRejection', (error) => unhandledRejectionFn(error))
973+
onTestFinished(() => {
974+
process.off('unhandledRejection', unhandledRejectionFn)
975+
})
976+
977+
const key = queryKey()
978+
const results: Array<string> = []
979+
980+
let mutationError: Error | undefined
981+
executeMutation(
982+
queryClient,
983+
{
984+
mutationKey: key,
985+
mutationFn: () => Promise.resolve('success'),
986+
onMutate: async () => {
987+
results.push('onMutate-async')
988+
await sleep(10)
989+
return { backup: 'async-data' }
990+
},
991+
onSuccess: async () => {
992+
results.push('onSuccess-async-start')
993+
await sleep(10)
994+
results.push('onSuccess-async-end')
995+
},
996+
onError: async () => {
997+
results.push('onError-async-start')
998+
await sleep(10)
999+
results.push('onError-async-end')
1000+
},
1001+
onSettled: () => {
1002+
results.push('local-onSettled')
1003+
},
1004+
},
1005+
'vars',
1006+
).catch((error) => {
1007+
mutationError = error
1008+
})
1009+
1010+
await vi.advanceTimersByTimeAsync(50)
1011+
1012+
expect(results).toEqual([
1013+
'onMutate-async',
1014+
'onSuccess-async-start',
1015+
'onSuccess-async-end',
1016+
'global-onSettled',
1017+
'onError-async-start',
1018+
'onError-async-end',
1019+
'global-onSettled',
1020+
'local-onSettled',
1021+
])
1022+
1023+
expect(unhandledRejectionFn).toHaveBeenCalledTimes(1)
1024+
expect(unhandledRejectionFn).toHaveBeenNthCalledWith(1, newMutationError)
1025+
1026+
expect(mutationError).toEqual(newMutationError)
1027+
})
1028+
1029+
test('error by mutations onSettled triggers onError callback, calling both onSettled callbacks twice', async ({
1030+
onTestFinished,
1031+
}) => {
1032+
const unhandledRejectionFn = vi.fn()
1033+
process.on('unhandledRejection', (error) => unhandledRejectionFn(error))
1034+
onTestFinished(() => {
1035+
process.off('unhandledRejection', unhandledRejectionFn)
1036+
})
1037+
1038+
const key = queryKey()
1039+
const results: Array<string> = []
1040+
1041+
const newMutationError = new Error('mutation-error')
1042+
1043+
let mutationError: Error | undefined
1044+
executeMutation(
1045+
queryClient,
1046+
{
1047+
mutationKey: key,
1048+
mutationFn: () => Promise.resolve('success'),
1049+
onMutate: async () => {
1050+
results.push('onMutate-async')
1051+
await sleep(10)
1052+
return { backup: 'async-data' }
1053+
},
1054+
onSuccess: async () => {
1055+
results.push('onSuccess-async-start')
1056+
await sleep(10)
1057+
results.push('onSuccess-async-end')
1058+
},
1059+
onError: async () => {
1060+
results.push('onError-async-start')
1061+
await sleep(10)
1062+
results.push('onError-async-end')
1063+
},
1064+
onSettled: async () => {
1065+
results.push('onSettled-async-promise')
1066+
await sleep(10)
1067+
throw newMutationError
1068+
},
1069+
},
1070+
'vars',
1071+
).catch((error) => {
1072+
mutationError = error
1073+
})
1074+
1075+
await vi.advanceTimersByTimeAsync(50)
1076+
1077+
expect(results).toEqual([
1078+
'onMutate-async',
1079+
'onSuccess-async-start',
1080+
'onSuccess-async-end',
1081+
'onSettled-async-promise',
1082+
'onError-async-start',
1083+
'onError-async-end',
1084+
'onSettled-async-promise',
1085+
])
1086+
1087+
expect(unhandledRejectionFn).toHaveBeenCalledTimes(1)
1088+
expect(unhandledRejectionFn).toHaveBeenNthCalledWith(1, newMutationError)
1089+
1090+
expect(mutationError).toEqual(newMutationError)
1091+
})
1092+
1093+
test('errors by onError and consecutive onSettled callbacks are transferred to different execution context where it are reported', async ({
1094+
onTestFinished,
1095+
}) => {
1096+
const unhandledRejectionFn = vi.fn()
1097+
process.on('unhandledRejection', (error) => unhandledRejectionFn(error))
1098+
onTestFinished(() => {
1099+
process.off('unhandledRejection', unhandledRejectionFn)
1100+
})
1101+
1102+
const globalErrorError = new Error('global-error-error')
1103+
const globalSettledError = new Error('global-settled-error')
1104+
1105+
queryClient = new QueryClient({
1106+
mutationCache: new MutationCache({
1107+
onError: () => {
1108+
throw globalErrorError
1109+
},
1110+
onSettled: () => {
1111+
throw globalSettledError
1112+
},
1113+
}),
1114+
})
1115+
queryClient.mount()
1116+
1117+
const key = queryKey()
1118+
const results: Array<string> = []
1119+
1120+
const newMutationError = new Error('mutation-error')
1121+
const newErrorError = new Error('error-error')
1122+
const newSettledError = new Error('settled-error')
1123+
1124+
let mutationError: Error | undefined
1125+
executeMutation(
1126+
queryClient,
1127+
{
1128+
mutationKey: key,
1129+
mutationFn: () => Promise.resolve('success'),
1130+
onMutate: async () => {
1131+
results.push('onMutate-async')
1132+
await sleep(10)
1133+
throw newMutationError
1134+
},
1135+
onSuccess: () => {
1136+
results.push('onSuccess-async-start')
1137+
},
1138+
onError: async () => {
1139+
results.push('onError-async-start')
1140+
await sleep(10)
1141+
throw newErrorError
1142+
},
1143+
onSettled: async () => {
1144+
results.push('onSettled-promise')
1145+
await sleep(10)
1146+
throw newSettledError
1147+
},
1148+
},
1149+
'vars',
1150+
).catch((error) => {
1151+
mutationError = error
1152+
})
1153+
1154+
await vi.advanceTimersByTimeAsync(30)
1155+
1156+
expect(results).toEqual([
1157+
'onMutate-async',
1158+
'onError-async-start',
1159+
'onSettled-promise',
1160+
])
1161+
1162+
expect(mutationError).toEqual(newMutationError)
1163+
1164+
expect(unhandledRejectionFn).toHaveBeenCalledTimes(4)
1165+
expect(unhandledRejectionFn).toHaveBeenNthCalledWith(1, globalErrorError)
1166+
expect(unhandledRejectionFn).toHaveBeenNthCalledWith(2, newErrorError)
1167+
expect(unhandledRejectionFn).toHaveBeenNthCalledWith(
1168+
3,
1169+
globalSettledError,
1170+
)
1171+
expect(unhandledRejectionFn).toHaveBeenNthCalledWith(4, newSettledError)
1172+
})
1173+
})
8451174
})

0 commit comments

Comments
 (0)