Describe the bug
Issue
Throwing from inside onError fails silently (no hint in the console about Uncaught Error).
However, using Promise.reject works as expected and the error can be seen in the console - "Uncaught error...".
This is the case for all three onError definitions:
- mutationCache
- useMutation
- mutate
And the second thing - throwing from inside onError defined in mutationCache also prevents onError defined in useMutation from running at all.
However, onError defined on mutate is called always:
- so no matter if we rejected or thrown from
mutationCache
- no matter if we rejected or thrown from
useMutation
onError in mutate will be called anyway.
Here's a codesandbox demo.
Your minimal, reproducible example
https://codesandbox.io/p/sandbox/sweet-villani-clpw7d?file=%2Fsrc%2FApp.tsx%3A52%2C8
Steps to reproduce
Code to reproduce the issue:
queryClient:
export const queryClient = new QueryClient({
mutationCache: new MutationCache({
onError: (error) => {
console.log("[mutationCache][onError] triggered with error: ", error);
try {
// do something that might throw an error
throw new Error("(ERROR A: thrown from mutationCache onError)");
} catch (e) {
console.log("[mutationCache][onError] caught e: ", e);
/**
* Throwing here (or simply not wrapping previous call in try/catch) causes it
* to stop here - so useMutation::onError in App.tsx is not even called
* and there is no hint in console apart from the explicit console.log(s) above
*/
throw e;
/**
* Instead of throwing, rejecting like this makes useMutation::onError in App.tsx run as expected,
* and after that there is clear "Uncaught (in promise) Error: (ERROR A: thrown from mutationCache onError)"
* shown in the console
*/
// Promise.reject(e);
}
},
}),
});
useMutation:
const { mutate } = useMutation({
mutationFn: async () => {
throw new Error("DUMMY POST REQUEST FAILED");
},
onError: (error) => {
/**
* If in index.tsx in MutationCache::onError we throw, we don't even get here,
* and there is no hint about uncaught error, only the explicit console.log(s) that are there.
*
* But if we use Promise.reject there, we do get here, so let's say we've done that
* and what comes next is the following:
*/
console.log("[useMutation][onError] triggered with error: ", error);
try {
// do something that might throw an error
throw new Error("(ERROR B: thrown from useMutation onError)");
} catch (e) {
console.log("[useMutation][onError] caught e: ", e);
/**
* Throwing here (or simply not wrapping previous call in try/catch) means
* that the only hint about ERROR B being thrown is the explicit console.log above.
* Then after those, the only thing we will see is:
* "Uncaught (in promise) Error: (ERROR A: thrown from mutationCache onError)"
* which comes from the mutationCache::onError in index.tsx
*/
throw e;
/**
* However, instead of throwing, if we reject, in the console we will see:
* "Uncaught (in promise) Error: (ERROR A: thrown from mutationCache onError)"
* followed by:
* "Uncaught (in promise) Error: (ERROR B: thrown from useMutation onError)"
*/
// Promise.reject(e);
}
},
});
mutate:
mutate(undefined, {
onError: (error) => {
/**
* Now for this onError inside mutate call, this one is called no matter
* if we throw or reject inside the mutationCache::onError.
* So if we reject in mutationCache, the callbacks are called in the following order:
* 1. mutationCache::onError
* 2. useMutation::onError
* 3. mutate::onError
*
* But if we throw in mutationCache, it's like this:
* 1. mutationCache::onError
* 2. mutate::onError
* So useMutation::onError is not called, but this one still is
*/
console.log("[mutate][onError] triggered with error: ", error);
try {
// do something that might throw an error
throw new Error("(ERROR C: thrown from useMutation onError)");
} catch (e) {
console.log("[mutate][onError] caught e: ", e);
/**
* But here it is the same as for the other cases,
* if we throw (or don't wrap previous call in try/catch), we won't see a hint about ERROR C apart from the console.log above
*/
throw e;
/**
* If we reject, we can see in the console:
* "Uncaught (in promise) Error: (ERROR C: thrown from useMutation onError)"
*/
// Promise.reject(e);
}
},
});
Expected behavior
I'd expect to see the Uncaught error in the console even if I use throw.
The case for me where I discovered this issue was that I was calling Object.keys(someObject.errors) inside onError in mutationCache and using a toast to show the error.
I also had some logic in onError defined in useMutation, setting an error for a form.
In one case the errors property was undefined, which caused Object.keys to throw before the toast could show anything - onError in useMutation was not called so no form error was set. So there was no hint at all about an error occurring.
So the only safe approach here seems to be wrapping the logic in onError in mutationCache in try/catch and Promise.reject the error from the catch clause - but this seems like an extra step that should not be necessary.
And having no hint about an error occurring seems like a bug.
How often does this bug happen?
Every time
Screenshots or Videos
No response
Platform
- OS: macOS
- Browser: Chrome
Tanstack Query adapter
react-query
TanStack Query version
v5.12.2
TypeScript version
No response
Additional context
No response
Describe the bug
Issue
Throwing from inside
onErrorfails silently (no hint in the console about Uncaught Error).However, using
Promise.rejectworks as expected and the error can be seen in the console - "Uncaught error...".This is the case for all three
onErrordefinitions:And the second thing - throwing from inside
onErrordefined inmutationCachealso preventsonErrordefined inuseMutationfrom running at all.However,
onErrordefined onmutateis called always:mutationCacheuseMutationonErrorinmutatewill be called anyway.Here's a codesandbox demo.
Your minimal, reproducible example
https://codesandbox.io/p/sandbox/sweet-villani-clpw7d?file=%2Fsrc%2FApp.tsx%3A52%2C8
Steps to reproduce
Code to reproduce the issue:
queryClient:
useMutation:
mutate:
Expected behavior
I'd expect to see the Uncaught error in the console even if I use throw.
The case for me where I discovered this issue was that I was calling
Object.keys(someObject.errors)insideonErrorinmutationCacheand using a toast to show the error.I also had some logic in
onErrordefined inuseMutation, setting an error for a form.In one case the
errorsproperty was undefined, which causedObject.keysto throw before the toast could show anything -onErrorinuseMutationwas not called so no form error was set. So there was no hint at all about an error occurring.So the only safe approach here seems to be wrapping the logic in
onErrorinmutationCacheintry/catchandPromise.rejectthe error from the catch clause - but this seems like an extra step that should not be necessary.And having no hint about an error occurring seems like a bug.
How often does this bug happen?
Every time
Screenshots or Videos
No response
Platform
Tanstack Query adapter
react-query
TanStack Query version
v5.12.2
TypeScript version
No response
Additional context
No response