You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Errored queries caught by ErrorBoundary are not retried on mount #2712
Describe the bug
I'm using Suspense and ErrorBoundaries and I'm seeing unexpected behavior when queries are erroring. If a query errors I'm displaying an ErrorBoundary for that part of the UI. Now if I navigate to another page using react-router-dom and navigate back to the page, the ErrorBoundary is still there and the query is not retried. I know that normally you would have to reset the error boundary while you are on the same page to trigger a re-run of the failed queries, but in this case I would've assumed that the queries are retried when I visit the page again, i.e. the components are mounted again.
Wait until the error occurs and the fallback of the ErrorBoundary is shown: "An error occured!" (You need to click off of react-error-overlay to see it)
Press the "Hide" button to unmount the ErrorBoundary
Press the "Show" button to render it again
Query is not re-executed and the error is immediately shown again
Expected behavior
The query is re-executed after the component mounts, i.e. honoring the retryOnMount setting.
Desktop (please complete the following information):
Whether you are using suspense or useErrorBoundaries in your queries, you will need a way to let queries know that you want to try again when re-rendering after some error occurred.
Thank you for the quick response @TkDodo !
I sort of got it working as you described, but in my actual application (not the example) the useQueryErrorResetBoundary doesn't work. I have to additionally reset the queries to actually trigger a proper refresh. Any thoughts/ideas what could be happening?
Code is looking like this:
exportfunctionuseResetReactQuery(){const{ reset }=useQueryErrorResetBoundary();constqueryClient=useQueryClient();constresetReactQuery=useCallback(async()=>{reset();// await queryClient.resetQueries();},[queryClient,reset]);returnresetReactQuery;}exportfunctionuseResetErrorBoundaryOnUnmount(){constreset=useResetReactQuery();useEffect(()=>{return()=>{reset();};},[reset]);}exportfunctionErrorFallback(){useResetErrorBoundaryOnUnmount();return<div>Fallback</div>;}// ...<ErrorBoundaryfallback={<ErrorFallback/>}><Suspensefallback={<LoadingSpinnercentered/>}><Information/>// this is the component using useQuery</Suspense></ErrorBoundary>
Closing the issue, since you've answered my initial question.
The example above from @TkDodo only works because the "hide" state does not also have a query. This is probably why @tobias-tengler was still seeing the issue in their real application.
Here is a modified example that shows the issue: https://codesandbox.io/s/nice-banach-x79q4r
Press "Toggle" a few times and you'll see that the failed query is never retried. If you comment out the useQuery hook in ComponentWithQuery2 then you'll see the first query is retried as expected.
I think you would need to put different keys on each QueryErrorResetBoundary in your updated example for react to create a new instance when toggling states. However, that still doesn't fix the problem: https://codesandbox.io/s/suspicious-grass-juez4e
Calling reset() only toggles that flag, it doesn't invalidate any cache or trigger a render. The query has already been unmounted at this point, so it never had an opportunity to notice that the flag changed.
yeah that makes sense - but it's still not working, is it? I don't really see why that is, because the scopes should be completely isolated by react context ...
something is off here because even if I remove query2 and the second error boundary completely, it still doesn't work:
I'm using error boundaries as well and I don't have such a problem, but I'm also using an explicit button to trigger the reset. I think the error boundary is just never reset 🤔
it seems that we cannot react to an errorBoundary unmounting? I have tried resetKeys and onResetKeysChange as per the docs, as setting the resetKeys to something static is not working.
I think this is also why it works with explicit reset buttons: because we then really call reset. You can see in this latest example that even though I'm using resetKeys, onResetKeysChange for number 1 is never called
If I'm reading this correctly, onResetKeysChange is not triggered if you go from no error to having an error. If you change query2 to also fail, both onResetKeysChange are called as expected.
In your example onResetKeysChanged is called during the transition from error to no error, which is when we want to invalidate the query.
If within onResetKeysChanged you call queryClient.clear() instead of reset() the query re-runs as expected. So the problem isn't reset not being called, but that it doesn't have the desired effect.
If you add a console.log statement inside ComponentWithQuery before useQuery, it prints when you toggle back to the error state. Doesn't this mean the ErrorBoundaryis being reset correctly but the query is immediately re-throwing?
function ComponentWithQuery() {
+ console.log("query!"); // would not see this if the ErrorBoundary did not reset
const { data } = useQuery(
The output is:
query!
The above error occurred in the <ComponentWithQuery> component:
resetting nr 2
in the unmount useEffect, we have the reset both the errorBoundary from react-error-boundary as well as the one from react-query.
One problem that remains is that each component needs to have its own "instance" of ErrorFallback, because otherwise, the component merely re-renders and never unmounts, so the effect doesn't run.
If we want to use the same FallbackComponent, we have to use resetKeys, as this example shows:
This thread really helped me to understand better how ErrorBoundary works. I was able to completely remove the QueryErrorResetBoundary and keep the reset to work, I'm not even sure it is needed as it works:
<ErrorBoundaryresetKeys={[location.pathname]}fallbackRender={(props)=>{const{ resetBoundary }=useErrorBoundary();return(<buttonclassName="bg-yellow-400 px-3 py-1 text-white"onClick={()=>{resetBoundary();}}>
Try again
</button>);}}><Outlet/></ErrorBoundary>
Re-rendered on route change inside ErrorBoundary
The issue I have is whenever I change the route (via a Link) it will re-render the previous route, thus it will try to refetch the failing query before mounting and rendering the new route. This slow down the application and seems to give some weird side-effects.
I'm using Tanstack Router with Tanstack Query.
As soon as I remove the resetKeys of course the ErrorBoundary doesn't re-render when the route change but then it forces the user to click on the button in order for the next route to be displayed.
From what I see with some helpful logs is that the location.pathname from useLocation() changes before the route is rendered in <Outlet /> I suspect that it is the reason why my "failing route" (it is the query fetch that is failing, not the route itself) re-render before the new route is rendered in <Outlet />.
Solution ? 🤔 ?
Any Idea how I could fix this issue? I will try to answer my own question : The ErrorBoundary should be at a lower level and not include the <Outlet /> as children.
The solution is working effectively. However this force me to wrap all my routes with an errorHandler instead of being able to set one for all children routes in one place using a pathless route.
I will try to set a reproducible example.
Edit: I forgot to mention that my way of fixing the initial issue (failing queries not retried on mount) was to use the @donrsh solution to removeQueries after a timeout but it seems a little bit hacky to be honest.
Just for information
I taught the behaviour changed but actually it was because I was using queryClient.prefetchQuery and it seems that with queryClient.prefetchQuery the errored queries are always retried.
I wonder what is the difference between the queryClient.prefetchQuery and a normal useSuspenseQuery.
Mmmh, thanks for the answer, I looked into it and actually I think I meant something a little different.
In my case I have the queryConfig with retry: false
I have a prefetch in an on hover of a dropdown selector. If the request fail and I hover again, it will try every time I hover.
However, if I don't do a prefetch and only fetch on mount (using useSuspenseQuery), it will never fetch again when I mount my component (this problem raised in this specific GitHub issue).
As I understand the issue here is that we would like to have query function to be executed again on mount if the query is in error state.
What I see is that the prefetchQuery doesn't have this problem and always execute the query function.
Not sure if my explanation is much better but as I don't have time to reproduce in a minimal example I have made a small video in order to demonstrate.
If the request fail and I hover again, it will try every time I hover.
yeah I think that’s expected. If the query has no data, it’s always considered stale, so it’ll prefetch. You would have to explicitly opt-out of error states. Maybe this is something that prefetchQuery should do on its own, not sure.
However, if I don't do a prefetch and only fetch on mount (using useSuspenseQuery), it will never fetch again when I mount my component (this problem raised in this specific GitHub issue).
Per your last comment, you’ve mentioned that you completely removed QueryErrorResetBoundary, and that would explain this behaviour. See, when you throw an error to an error boundary, react will continue to re-render the error’d component at least one more time. That’s a react implementation detail, which shouldn’t matter, but it does, because we would immediately start fetching the error’d query again. If react were to just render the error boundary fallback immediately after we throw the error, this wouldn’t be necessary.
That’s why we “lock” a query from automatic refetches until “approved” by dismissing the error boundary, and that happens imperatively with the reset method we provide from QueryErrorResetBoundary.
If you don’t do that, the Query will stay “locked” and won’t re-fetch when the component mounts.
Yes since my last comment I have put back the QueryErrorResetBoundary exactly because of this issue. I was not able to reset the errored query. For an UX perspective it is quite nice to show the user what is happening and allowing him to try again.
You would have to explicitly opt-out of error states
How would you do that? I have done my search but could not find out
Maybe this is something that prefetchQuery should do on its own, not sure.
Not sure as well, I think we should have a way to do that maybe with an option? But I don't want to screw the API 😄 (yes I read your slides)
Hey I noticed this issue as well when working in the past on a dynamic component which renders triggers a query and if it's failed, retry the queries again. I will try to fix this if I can let's see.
#10079 is related, but I don't think it should be treated as the full fix for #2712.
It targets the useQuery().promise + Suspense/ErrorBoundary reset path:
recreate a fresh pending promise when retry transitions the query back to fetching
explicitly refetch active/enabled errored queries on QueryErrorResetBoundary reset
The remaining #2712 behavior around remounted sibling queries consuming/clearing the reset signal seems better handled by a per-query or per-observer reset generation mechanism, like the direction explored in #10375 and now #10921.
So I’d keep #10079 scoped to #9978 / promise reset behavior, and handle the reset-generation semantics separately.
Activity
TkDodo commentedon Sep 24, 2021
The docs say:
So I think in your scenario, utilizing
useQueryErrorResetBoundarywhen the ErrorBoundary unmounts should work. Here is a working sandbox: https://codesandbox.io/s/naughty-nash-7jixq?file=/src/App.tsxtobias-tengler commentedon Sep 24, 2021
Thank you for the quick response @TkDodo !
I sort of got it working as you described, but in my actual application (not the example) the
useQueryErrorResetBoundarydoesn't work. I have to additionally reset the queries to actually trigger a proper refresh. Any thoughts/ideas what could be happening?Code is looking like this:
Closing the issue, since you've answered my initial question.
codebutler commentedon Oct 7, 2022
Hi @TkDodo @tannerlinsley,
I think this issue was closed too soon.
The example above from @TkDodo only works because the "hide" state does not also have a query. This is probably why @tobias-tengler was still seeing the issue in their real application.
Here is a modified example that shows the issue: https://codesandbox.io/s/nice-banach-x79q4r
Press "Toggle" a few times and you'll see that the failed query is never retried. If you comment out the
useQueryhook inComponentWithQuery2then you'll see the first query is retried as expected.What seems to be happening:
This inconsistency feels unintuitive, was it intentional? What is the proper workaround?
Thanks.
TkDodo commentedon Oct 7, 2022
yeah, this is weird. I was thinking that with two ErrorBoundaries, you would also need two reset contexts, like I've done here:
https://codesandbox.io/s/sparkling-bird-4t4zoq?file=/src/App.tsx
But it still doesn't work, not sure ...
codebutler commentedon Oct 7, 2022
Thanks for taking another look at this!
I think you would need to put different
keys on eachQueryErrorResetBoundaryin your updated example for react to create a new instance when toggling states. However, that still doesn't fix the problem: https://codesandbox.io/s/suspicious-grass-juez4eCalling
reset()only toggles that flag, it doesn't invalidate any cache or trigger a render. The query has already been unmounted at this point, so it never had an opportunity to notice that the flag changed.TkDodo commentedon Oct 7, 2022
yeah that makes sense - but it's still not working, is it? I don't really see why that is, because the scopes should be completely isolated by react context ...
something is off here because even if I remove query2 and the second error boundary completely, it still doesn't work:
https://codesandbox.io/s/suspicious-wilson-reie3j?file=/src/App.tsx
I'm using error boundaries as well and I don't have such a problem, but I'm also using an explicit button to trigger the reset. I think the error boundary is just never reset 🤔
codebutler commentedon Oct 7, 2022
You have a typo in here:
...but that also doesn't fix the problem unless you also remove the
QueryErrorResetBoundarycompletely. 🤔TkDodo commentedon Oct 7, 2022
yeah sorry, that was stupid. some more attempts happening here:
https://codesandbox.io/s/blue-haze-ijtqt1?file=/src/App.tsx
it seems that we cannot react to an errorBoundary unmounting? I have tried
resetKeysandonResetKeysChangeas per the docs, as setting theresetKeysto something static is not working.I think this is also why it works with explicit reset buttons: because we then really call
reset. You can see in this latest example that even though I'm using resetKeys,onResetKeysChangefor number 1 is never calledcodebutler commentedon Oct 7, 2022
If I'm reading this correctly,
onResetKeysChangeis not triggered if you go from no error to having an error. If you change query2 to also fail, bothonResetKeysChangeare called as expected.codebutler commentedon Oct 7, 2022
In your example
onResetKeysChangedis called during the transition from error to no error, which is when we want to invalidate the query.If within
onResetKeysChangedyou callqueryClient.clear()instead ofreset()the query re-runs as expected. So the problem isn'tresetnot being called, but that it doesn't have the desired effect.TkDodo commentedon Oct 8, 2022
so as discussed here, it seems that
resetKeys/onResetKeysChangedis not good enough:having an effect that that listens to whatever the reset keys are and reset the boundary should work, but I can't get it to work :/
https://codesandbox.io/s/sharp-surf-2uu0kd?file=/src/App.tsx
codebutler commentedon Oct 10, 2022
If you add a
console.logstatement insideComponentWithQuerybeforeuseQuery, it prints when you toggle back to the error state. Doesn't this mean theErrorBoundaryis being reset correctly but the query is immediately re-throwing?function ComponentWithQuery() { + console.log("query!"); // would not see this if the ErrorBoundary did not reset const { data } = useQuery(The output is:
so the reset is happening after the query.
TkDodo commentedon Jan 27, 2023
looked into this again today because we're also facing it now, here is an altered example that seems to work:
changes I made:
FallbackComponentinstead offallbackerrorBoundaryfromreact-error-boundaryas well as the one fromreact-query.One problem that remains is that each component needs to have its own "instance" of
ErrorFallback, because otherwise, the component merely re-renders and never unmounts, so the effect doesn't run.If we want to use the same
FallbackComponent, we have to useresetKeys, as this example shows:20 remaining items
Ganbin commentedon Sep 7, 2024
This thread really helped me to understand better how ErrorBoundary works. I was able to completely remove the
QueryErrorResetBoundaryand keep the reset to work, I'm not even sure it is needed as it works:Re-rendered on route change inside
ErrorBoundaryThe issue I have is whenever I change the route (via a
Link) it will re-render the previous route, thus it will try to refetch the failing query before mounting and rendering the new route. This slow down the application and seems to give some weird side-effects.I'm using Tanstack Router with Tanstack Query.
As soon as I remove the
resetKeysof course theErrorBoundarydoesn't re-render when the route change but then it forces the user to click on thebuttonin order for the next route to be displayed.From what I see with some helpful logs is that the
location.pathnamefromuseLocation()changes before the route is rendered in<Outlet />I suspect that it is the reason why my "failing route" (it is the query fetch that is failing, not the route itself) re-render before the new route is rendered in<Outlet />.Solution ? 🤔 ?
Any Idea how I could fix this issue? I will try to answer my own question : The
ErrorBoundaryshould be at a lower level and not include the<Outlet />as children.The solution is working effectively. However this force me to wrap all my routes with an errorHandler instead of being able to set one for all children routes in one place using a pathless route.
I will try to set a reproducible example.
Edit: I forgot to mention that my way of fixing the initial issue (failing queries not retried on mount) was to use the @donrsh solution to
removeQueriesafter a timeout but it seems a little bit hacky to be honest.Ganbin commentedon Nov 22, 2024
Just for information
I taught the behaviour changed but actually it was because I was using
queryClient.prefetchQueryand it seems that withqueryClient.prefetchQuerythe errored queries are always retried.I wonder what is the difference between the
queryClient.prefetchQueryand a normaluseSuspenseQuery.TkDodo commentedon Nov 22, 2024
prefetching doesn’t retry a query unless you specify so. That is explicitly handled here:
query/packages/query-core/src/queryClient.ts
Lines 340 to 343 in 92a109d
Ganbin commentedon Nov 22, 2024
Mmmh, thanks for the answer, I looked into it and actually I think I meant something a little different.
In my case I have the
queryConfigwithretry: falseI have a prefetch in an on hover of a dropdown selector. If the request fail and I hover again, it will try every time I hover.
However, if I don't do a prefetch and only fetch on mount (using
useSuspenseQuery), it will never fetch again when I mount my component (this problem raised in this specific GitHub issue).As I understand the issue here is that we would like to have query function to be executed again on mount if the query is in error state.
What I see is that the
prefetchQuerydoesn't have this problem and always execute the query function.Not sure if my explanation is much better but as I don't have time to reproduce in a minimal example I have made a small video in order to demonstrate.
2024-11-22_16-48-31.mp4
TkDodo commentedon Nov 22, 2024
yeah I think that’s expected. If the query has no data, it’s always considered stale, so it’ll prefetch. You would have to explicitly opt-out of error states. Maybe this is something that
prefetchQueryshould do on its own, not sure.Per your last comment, you’ve mentioned that you completely removed
QueryErrorResetBoundary, and that would explain this behaviour. See, when you throw an error to an error boundary, react will continue to re-render the error’d component at least one more time. That’s a react implementation detail, which shouldn’t matter, but it does, because we would immediately start fetching the error’d query again. If react were to just render the error boundary fallback immediately after we throw the error, this wouldn’t be necessary.That’s why we “lock” a query from automatic refetches until “approved” by dismissing the error boundary, and that happens imperatively with the
resetmethod we provide fromQueryErrorResetBoundary.If you don’t do that, the Query will stay “locked” and won’t re-fetch when the component mounts.
Ganbin commentedon Nov 26, 2024
Ok, thanks for the explanation.
Yes since my last comment I have put back the
QueryErrorResetBoundaryexactly because of this issue. I was not able to reset the errored query. For an UX perspective it is quite nice to show the user what is happening and allowing him to try again.How would you do that? I have done my search but could not find out
Not sure as well, I think we should have a way to do that maybe with an option? But I don't want to screw the API 😄 (yes I read your slides)
TkDodo commentedon Nov 26, 2024
I have another (hacky) idea to try and get rid of the
QueryErrorResetBoundary. Will try to work on it this week.Guuz commentedon Apr 9, 2025
Hi @TkDodo , last November you had an idea. Did anything come out of that? :) We are running into this issue as well.
Thanks!
TkDodo commentedon Apr 9, 2025
sadly no, the idea I had (like all other ideas to make this better) did not work out 😂
numanjvd82 commentedon Jul 13, 2025
Hey I noticed this issue as well when working in the past on a dynamic component which renders triggers a query and if it's failed, retry the queries again. I will try to fix this if I can let's see.
robkozura commentedon Dec 17, 2025
Here is a
useEffectworkaround:The idea is to delay the error boundary reset for another render
ousamabenyounes commentedon Apr 26, 2026
zaewc commentedon Jun 18, 2026
#10079 is related, but I don't think it should be treated as the full fix for #2712.
It targets the
useQuery().promise+ Suspense/ErrorBoundary reset path:QueryErrorResetBoundaryresetThe remaining #2712 behavior around remounted sibling queries consuming/clearing the reset signal seems better handled by a per-query or per-observer reset generation mechanism, like the direction explored in #10375 and now #10921.
So I’d keep #10079 scoped to #9978 / promise reset behavior, and handle the reset-generation semantics separately.