Skip to content

Errored queries caught by ErrorBoundary are not retried on mount #2712

Open

Description

@tobias-tengler

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.

To Reproduce

  1. Visit the Code Sandbox
  2. Open the console
  3. 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)
  4. Press the "Hide" button to unmount the ErrorBoundary
  5. Press the "Show" button to render it again
  6. 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):

  • Version: 3.24.4

Activity

  1. TkDodo commented on Sep 24, 2021

    @TkDodo
    Collaborator

    The docs say:

    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.

    So I think in your scenario, utilizing useQueryErrorResetBoundary when the ErrorBoundary unmounts should work. Here is a working sandbox: https://codesandbox.io/s/naughty-nash-7jixq?file=/src/App.tsx

  2. tobias-tengler commented on Sep 24, 2021

    @tobias-tengler
    Author

    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:

    export function useResetReactQuery() {
        const { reset } = useQueryErrorResetBoundary();
        const queryClient = useQueryClient();
    
        const resetReactQuery = useCallback(async () => {
            reset();
            // await queryClient.resetQueries();
        }, [queryClient, reset]);
    
        return resetReactQuery;
    }
    
    export function useResetErrorBoundaryOnUnmount() {
        const reset = useResetReactQuery();
    
        useEffect(() => {
            return () => {
                reset();
            };
        }, [reset]);
    }
    
    export function ErrorFallback() {
        useResetErrorBoundaryOnUnmount();
    
        return <div>Fallback</div>;
    }
    
    // ...
    <ErrorBoundary fallback={<ErrorFallback />}>
      <Suspense fallback={<LoadingSpinner centered />}>
        <Information /> // this is the component using useQuery
      </Suspense>
    </ErrorBoundary>

    Closing the issue, since you've answered my initial question.

  3. codebutler commented on Oct 7, 2022

    @codebutler

    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 useQuery hook in ComponentWithQuery2 then you'll see the first query is retried as expected.

    What seems to be happening:

    1. On unmount, query1 calls reset() which sets the isReset flag.
    2. On mount, query2 immediately resets the flag. If there is no query2 the flag remains set and query1 will be retried.

    This inconsistency feels unintuitive, was it intentional? What is the proper workaround?

    Thanks.

  4. TkDodo commented on Oct 7, 2022

    @TkDodo
    Collaborator

    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 ...

  5. reopened this on Oct 7, 2022
  6. codebutler commented on Oct 7, 2022

    @codebutler

    Thanks for taking another look at this!

    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.

  7. TkDodo commented on Oct 7, 2022

    @TkDodo
    Collaborator

    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 🤔

  8. codebutler commented on Oct 7, 2022

    @codebutler

    https://codesandbox.io/s/suspicious-wilson-reie3j?file=/src/App.tsx

    You have a typo in here:

    - const reset = useQueryErrorResetBoundary();
    + const { reset } = useQueryErrorResetBoundary();

    ...but that also doesn't fix the problem unless you also remove the QueryErrorResetBoundary completely. 🤔

  9. TkDodo commented on Oct 7, 2022

    @TkDodo
    Collaborator

    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 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

  10. codebutler commented on Oct 7, 2022

    @codebutler

    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.

  11. codebutler commented on Oct 7, 2022

    @codebutler

    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.

  12. TkDodo commented on Oct 8, 2022

    @TkDodo
    Collaborator

    so as discussed here, it seems that resetKeys / onResetKeysChanged is 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

  13. codebutler commented on Oct 10, 2022

    @codebutler

    so as discussed here, it seems that resetKeys / onResetKeysChanged is 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

    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 ErrorBoundary is 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
    

    so the reset is happening after the query.

  14. TkDodo commented on Jan 27, 2023

    @TkDodo
    Collaborator

    looked into this again today because we're also facing it now, here is an altered example that seems to work:

    changes I made:

    • use FallbackComponent instead of fallback
    • 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:

  15. 20 remaining items

  16. Ganbin commented on Sep 7, 2024

    @Ganbin

    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:

    <ErrorBoundary
      resetKeys={[location.pathname]}
      fallbackRender={(props) => {
        const { resetBoundary } = useErrorBoundary();
        return (
          <button
            className="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.

  17. Ganbin commented on Nov 22, 2024

    @Ganbin

    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.

  18. TkDodo commented on Nov 22, 2024

    @TkDodo
    Collaborator

    prefetching doesn’t retry a query unless you specify so. That is explicitly handled here:

    // https://github.com/tannerlinsley/react-query/issues/652
    if (defaultedOptions.retry === undefined) {
    defaultedOptions.retry = false
    }

  19. Ganbin commented on Nov 22, 2024

    @Ganbin

    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.

    2024-11-22_16-48-31.mp4
  20. TkDodo commented on Nov 22, 2024

    @TkDodo
    Collaborator

    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.

  21. Ganbin commented on Nov 26, 2024

    @Ganbin

    Ok, thanks for the explanation.

    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)

  22. TkDodo commented on Nov 26, 2024

    @TkDodo
    Collaborator

    I have another (hacky) idea to try and get rid of the QueryErrorResetBoundary. Will try to work on it this week.

  23. Guuz commented on Apr 9, 2025

    @Guuz

    I have another (hacky) idea to try and get rid of the QueryErrorResetBoundary. Will try to work on it this week.

    Hi @TkDodo , last November you had an idea. Did anything come out of that? :) We are running into this issue as well.
    Thanks!

  24. TkDodo commented on Apr 9, 2025

    @TkDodo
    Collaborator

    sadly no, the idea I had (like all other ideas to make this better) did not work out 😂

  25. numanjvd82 commented on Jul 13, 2025

    @numanjvd82

    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.

  26. robkozura commented on Dec 17, 2025

    @robkozura

    Here is a useEffect workaround:

    import { type PropsWithChildren, useEffect, useState } from 'react'
    import { ErrorBoundary } from 'react-error-boundary'
    import { useLocation } from 'react-router-dom'
    import { QueryErrorResetBoundary } from '@tanstack/react-query'
    
    export const QueryErrorBoundary = ({ children }: PropsWithChildren) => {
      const { key } = useLocation()
      const [resetKey, setResetKey] = useState(key)
    
      // Delays error boundary reset until after the route changes
      useEffect(() => {
        setResetKey(key)
      }, [key])
    
      return (
        <QueryErrorResetBoundary>
          {({ reset }) => (
            <ErrorBoundary
              onReset={reset}
              resetKeys={[resetKey]}
              fallbackRender={({ resetErrorBoundary }) => {
                return <button onClick={resetErrorBoundary}>Reset</button>
              }}
            >
              {children}
            </ErrorBoundary>
          )}
        </QueryErrorResetBoundary>
      )
    }
    
    

    The idea is to delay the error boundary reset for another render

  27. ousamabenyounes commented on Apr 26, 2026

    @ousamabenyounes
  28. zaewc commented on Jun 18, 2026

    @zaewc
    Contributor

    #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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions