Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

blog/react-query-error-handling #29

Closed
utterances-bot opened this issue Sep 10, 2021 · 39 comments
Closed

blog/react-query-error-handling #29

utterances-bot opened this issue Sep 10, 2021 · 39 comments

Comments

@utterances-bot
Copy link

React Query Error Handling | TkDodo's blog

After covering the sunshine cases of data fetching, it's time to look at situations where things don't go as planned and "Something went wrong..."

https://tkdodo.eu/blog/react-query-error-handling?utterances=7f4ddb690c1dc8b340d5813cNdwcVH1AM%2B%2F%2BNm%2F7plRpwS567856B8nwLLdwEWUkZUEMc0yQTcWPy9sNBGdEH2aBVE4P4kvQZA92KLug9dFRt6gblMCB9Gikd9jEf3ZczDWkqifI7DcRPO7bs4M%3D

Copy link

Thanks for yet another great article!
I learned a ton by reading every react-query article. This was time well spent for me.

Copy link

Thanks, this is really helpful! I now understand error boundaries a lot better.

If I use onError in the queryClient to globally catch all errors as you suggest, is there any way to get the query key from react query for error reporting purposes? Or am I limited to just the returned error message itself?

@TkDodo
Copy link
Owner

TkDodo commented Sep 23, 2021

@ptmkenny the signature of onError is:

onError?: (error: unknown, query: Query) => void

so it gets the whole query as a second parameter, where you'll also find the queryKey.

@ptmkenny
Copy link

Wow, I only thought error was available. That's a huge help and now I can replace about a 100 lines of code where I was tracking errors individually. Many thanks!

Copy link

bsides commented Sep 25, 2021

Awesome artcile, thank you.
A question though, how would you do the QueryCache thing on a SSR environment, like Nextjs, when using dehydratedState?

@TkDodo
Copy link
Owner

TkDodo commented Sep 25, 2021

how would you do the QueryCache thing on a SSR environment, like Nextjs, when using dehydratedState?

I guess it depends on what you want to do when a query errors on the server side. Generally, I'd say since you create a separate QueryClient on the frontend / backend, you can do the same and potentially share the code. Keep in mind that per default, only successful queries are sent to the client though.

Copy link

NikitaIT commented Oct 4, 2021

Cool article. Particularly good is that you mentioned Error Boundaries for 5xx. Sometimes I hear Error Boundaries being used to handle errors, but rarely do anyone mention that they are only meant to adhere to the Fail-Fast Principle.

Copy link

lamaland commented Oct 7, 2021

Hi, Reading this thread, I found out that a "query" param is available for the onError callback?
Can't figure out how to use it :(

Looking at the react-query typings (QueryObserverOptions), i see only :

/**
     * This callback will fire if the query encounters an error and will be passed the error.
     */
    onError?: (err: TError) => void;

Tried to bypass typings by casting options to any, but logging the query parameter always outputs undefined.

Some clarifications would be very sweet from you guys :)
(using latest version : 3.25.1)

@TkDodo
Copy link
Owner

TkDodo commented Oct 7, 2021

@lamaland Seems like you’re mixing up the global onError callback on the queryCache, which does receive the query as second parameter, and the callback on observer level (onError of useQuery), which only receives the error.

Copy link

lamaland commented Oct 7, 2021

@TkDodo, my bad, you are absolutly right 👍

Copy link

lamaland commented Oct 7, 2021

Was trying like this :

  const reactQueryClient = new QueryClient({
    defaultOptions: {
      queries: {
        onError: (error) => {
          ...
        },
      },
    },
  })

Using the querycache way works as expected :)

Copy link

So if I do this either inside of QueryClient or QueryCache:

const reactQueryClient = new QueryClient({
    defaultOptions: {
      queries: {
        onError: (error) => {
          ...
        },
      },
    },
  })

the other default options provided by react-query are gone (for example default refetch when the window becomes active). How can one add a global error handler without overwriting the out-of-the-box defaults?

@TkDodo
Copy link
Owner

TkDodo commented Oct 8, 2021

No, the default options you provide are merged with the default options, so unless you set refetchOnWindowFocus to false, it will stay on.

Further, this is not the way to provide a global onError handler. As the article tried to explain, you have to set onError on the QueryCache or the MutationCache for a truly global handler.

Copy link

Ok, so if I do this the refetchOnWindowFocus works but obviously i don't get any errors set into state since it's the first time the app loads and query.state.data is undefined:

  const queryClient = new QueryClient({
    queryCache: new QueryCache({
      onError: (error: any, query: any) => {
        if (query.state.data !== undefined) {
          setErrors([...errors, ...error.response.data.errors])
        }
      },
    }),
  })

but if I do this the errors are set globally, but the refetchOnWindowFocus no longer works:

  const queryClient = new QueryClient({
    queryCache: new QueryCache({
      onError: (error: any, query: any) => {
        setErrors([...errors, ...error.response.data.errors])
      },
    }),
  })

can you let me know what i'm missing?

@TkDodo
Copy link
Owner

TkDodo commented Oct 9, 2021

Please create a codesandbox reproduction so that I can get the full picture.

Copy link

vago commented Oct 11, 2021

Superb article, thanks!
I like the tip about background refetches towards the end. Have a continuing question regarding "keep the stale UI intact" for background refetches. What would you say is a good practice to show stale UI. Previously, in my component, I just checked for query.isSuccess to render content, but in case of background refetch failure, this will evaluate to false, hence no content would show. Would you say checking for query.data !== undefined instead of isSuccess would be a better way?

@TkDodo
Copy link
Owner

TkDodo commented Oct 11, 2021

@vago it's a good question, and one that doesn't have a clearly defined answer imo, as it depends on the kind of data that you display. Is stale data better than an error screen, and can background errors be discarded or displayed otherwise? If so, my first check is always if (query.data) and I then return components showing that data.

In other cases, if I'm getting an error after 3 retries, maybe there is a real error and I don't want my users to see, and interact with, the stale ui. In that cases doing the status check is perfectly fine.

I've also written about that here: https://tkdodo.eu/blog/status-checks-in-react-query

@vago
Copy link

vago commented Oct 11, 2021

Thank you for the reasoning. Makes sense.

@danielbartsch
Copy link

How would you show a specific toast error notification tho, that is eg. different for every query?
onError does have the query, but you can't store any custom additional information in it, can you?

I think the best way to do this is, within useQuery or useMutation, just add a .catch and put it there.
Is there a better way?

@TkDodo
Copy link
Owner

TkDodo commented Nov 6, 2021

hey @danielbartsch 👋

glad to see you're still reading my blog 😄. In v3.29.0, we added the meta field to queries (note that we did that before facebook announced the rename 😆). It's an object where you can "store" arbitrary meta information for a query. It is injected into the QueryFunctionContext, and it's also available everywhere the query is available.

I think it would be a good solution for your use-case. Example:

const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: (error, query) => {
      toast.error(`${query.meta.myMessage}: ${error.message}`)
    },
  }),
})
useQuery(key, fn, { meta: { myMessage: "..." })

before that, the only "workaround" was to make that message part of the queryKey. Let me know what you think :)

@danielbartsch
Copy link

great solution! However meta doesn't seem to exist yet for useMutation, but that can fail as well 🤔

Also is there a way of ensuring typescript types for this, other than wrapping useQuery and defining your own meta types? (eg. so that myMessage will autosuggest when you add a meta object to the options parameter of useQuery)

@TkDodo
Copy link
Owner

TkDodo commented Nov 8, 2021

However meta doesn't seem to exist yet for useMutation, but that can fail as well

yep, we'd need to add that. Would you like to contribute it?

Also is there a way of ensuring typescript types for this

Thought about this when we introduced it, but decided against it. We've done that for the query key, and we needed to add a 4th generic to useQuery, meta would need a 5th one. Also, it wouldn't help you in the global error handler, because that one would run for all queries. There is no way to guarantee that all queries have a meta of a certain type.

@danielbartsch
Copy link

for readers of this blog: it's done

Copy link

Thanks for letting us know about what is the right way to handle errors if using react-query. But I have a question related to global onError callback handling mechanism. Suppose if we need to send two pieces of information to the toast so that it can display both a. error message b. error title.

a. error message is what we we are getting from server. Good enough!
b. error title: It is not something that the server sends. It is "local" to the client and client decides what it should be exactly for e.g. when a login call is made to the server and it fails, the title could be "Authentication Failed" Or in case of "create user" call failure, it could be "Validation Failed". Could you pl. guide how and from where to inject/sent this piece of info i.e. title to the onError global callback?

Thanks

@TkDodo
Copy link
Owner

TkDodo commented Dec 16, 2021

Could you pl. guide how and from where to inject/sent this piece of info i.e. title to the onError global callback?

@Ramandhingra as discussed above, you can use the meta field for that.

@cloudcompute
Copy link

cloudcompute commented Dec 17, 2021

@TkDodo

I read the "meta" feature and it is clear to me how to inject a title. Thanks

I have few other Qs for you. Could you pl. answer them:

a. Is it possible to execute the onError both at a global and local level for a given query or mutation. For example, use global onError to display toast and local onError to handle local logic, for example navigate to this particular route if error occurs.

b. Is it possible to execute global onError conditionally, like I do not want to show a toast for some errors. I think it might be achieved by adding a boolean key/value in the 'meta' object and check if it is set to true, then only display toast. Right?

c. Here is an example that displays toast locally:

const login = async ({ email, password }) => {
  // Make an asynchrounous call to the server
  ...
  onError: (err) {
    // Local error handling
    displayToast("error", "Authentication Failed", err.errorMessage);
  }
}

Now as advised by you, we can also display toasts at a global level indicating that some validation failed. Keeping this in view, I am looking for code something like this:

const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: (error, query) => {
      // show error toasts if react-query's background fetch fails
      if (query.state.data !== undefined) {
        displayToast("error", "an appropriate title like... Server not reachable", "an appropriate error message")
      }
      // display error toast for ALL of our queries and mutations.. at a single place here meaning no local onError handler
      else {
        //title: Authentication Failed.. fetched from meta
        displayToast("error", title, error.message)
      }
    },
  }),
})

As of now, I don't know react-query much. So pl. confirm if the if/else logic is correct.

Is this pattern okay with you, or would you advise local onError handler for each query and mutation placed immediately after the server response in the local functions themselves?

d. If using react-query, is there any way to check isLoading at a global level.. just like react-query has provided us with the global onError callback? I guess there is no such event handler that is fired just before the query/mutation execution is about to take place, named something like onBefore. If not there, could you pl. add it in next release?

I know that React.Suspense can be used but it is kinda experimental and React does not recommend to use it. How about your opinion, can we use Suspense in a production app where there will be about 100 queries and mutations resulting in a lot of boilerplate if Loading is handled locally?

e. In order to display toast for failed background queries, we are writing a global onError callback for "QueryCache". For the same reason, it needs to be written for MutationCache as well. Am I right?

f. Do you recommend a single Error Boundary component for the entire app? I am asking this because I read it somewhere that it is not advisable to have a single global Error Boundary.

If really not advisable, consider this typical component hierarchy where QueryClientProvider is at the top. Now if we wrap the QueryClientProvider component inside the Error Boundary component, the 5xx errors thrown by react-query are handled by the global Error Boundary nonetheless.

<QueryClientProvider client={queryClient}>
  <BrowserRouter>
    <AuthProvider>
      <Toaster />
      <Routes />
    </AuthProvider>
  </BrowserRouter>
</QueryClientProvider>

Thanks for your guidance and time.

Copy link

@TkDodo
Could you pl. reply the questions I asked above especially a. to d. Thanks.

@TkDodo
Copy link
Owner

TkDodo commented Dec 30, 2021

@Ramandhingra sorry for taking a bit longer. For posterity: I have answered the questions on a discussion in the react-query repo.

Copy link

@TkDodo

Yes, I got to know by email that you answered my queries over there.

Thanks

Copy link

drmeloy commented Jan 18, 2022

@TkDodo
Quick question: Does the instantiation of queryClient need to happen outside of a component or can it be done within a component? I ask because I have a function that generates an error toast with the provided message, but I receive it from a hook, so in order to get that function into the global error handling I would need it to be within a functional component? Thanks

@TkDodo
Copy link
Owner

TkDodo commented Jan 18, 2022

you can definitely create the QueryClient inside the component, but make sure to memoize it correctly with either useRef or useState - not useMemo.

You can read about that here: https://tkdodo.eu/blog/use-state-for-one-time-initializations

Resource in that article is basically a QueryClient ;)

Copy link

drmeloy commented Jan 18, 2022

@TkDodo
Awesome! Thank you! One follow up, if a query is put into an error state it successfully generates an error toast for me with the method described above. However, it seems that the error state persists in a way that I cannot dismiss the error, meaning when I dismiss the error toast another one is immediately generated. I believe this is because that query is still in an error state? Is there a way to clear the error state of a query or queries? Thank you

@TkDodo
Copy link
Owner

TkDodo commented Jan 18, 2022

@drmeloy I would need to see an example please. Usually state management of toast notifications is outside of react-query so I have no idea what could cause that behavior

Copy link

drmeloy commented Jan 18, 2022

Sure! I have collection and management of toasts being done through a react context provider. It provides a function addErrorToast to add a negative toast object to the useState array, which is what the <MultiToasts> component displays:

const ToastsContext = createContext<ContextValue>(null);

export function ToastsProvider({
  children,
}: {
  children: React.ReactNode;
}): JSX.Element {
  const [items, setItems] = useState<ToastObject[]>([]);

  useEffect(() => {
    console.log('ITEMS: ', items);
  }, [items]);

  const addToast = (toast: ToastObject) => {
    if (items.some(item => item.content === toast.content)) return;
    setItems((prev) => [toast, ...prev]);
  }

  const removeToast = (id: string) => {
    setItems((prev) => prev.filter((toast) => toast.id !== id));
  }

// type is 'unknown' here so that React Query onError can pass its error
  const addErrorToast = (arg0: unknown) => {
    const content = typeof arg0 === 'string' ? arg0 : 'Unknown error';
    const id = `${Math.random()}${new Date().getTime()}`;
    const toast: ToastObject = {
      actions: [
        {
          onClick: () => removeToast(id),
          text: 'Dismiss',
        },
      ],
      content,
      id,
      variant: TOAST_VARIANTS.NEGATIVE,
      visibilityDuration: VISIBILITY_DURATIONS.INFINITE,
    };
    addToast(toast);
  };

  const toasts = (
    <MultiToasts items={items} position={TOAST_POSITIONS.INLINE} />
  );

  const contextValue = {
    toasts,
    addErrorToast,
  };

  return (
    <ToastsContext.Provider value={contextValue}>
      {children}
    </ToastsContext.Provider>
  );
}

export const useToasts = (): ToastsValues => {
  const context = useContext(ToastsContext);
  if (context === null)
    throw new Error('useToasts must be used within a <ToastProvider>');
  return context;
};

You'll notice the removeToast function that is being used on the actions field of the toast object within the addErrorToast function. With the useEffect that I have at the top of the file I can see that clicking "Dismiss" on the toast indeed removes that toast object from the items array, which I would then expect to make the component disappear as the items array it uses is now empty. However, the behavior I'm seeing is that when I click "Dismiss" the items array is momentarily empties, but then immediately populated by a new error. I am using mock service worker to make a single endpoint fail, so it's the same endpoint, and I have verified that the endpoint is not being called again upon clicking "Dismiss" on the error toast.

Here is my Query Provider file:

export const QueryProvider = ({ children }: { children: React.ReactNode }): JSX.Element=> {
  const { addErrorToast } = useToasts();
  const [queryClient] = useState(() => new QueryClient({
    queryCache: new QueryCache({
      onError: (error) => addErrorToast(error),
    }),
  }));

  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  )
}

My observations so far:

  1. Two error toasts are being generated from this! One that says "Unknown error" (meaning it's being passed an error that isn't of type string? Perhaps it comes in as some unknown first and then the error string from my API comes after?)
  2. I can dismiss the first one "Unknown error" and it remains dismissed.
  3. I can dismiss the second one "My error message" but it is IMMEDIATELY replaced. It does not even flicker in the UI as having been dismissed, but I can see via the console.log in my useEffect that the items array is empty for a brief moment.

Copy link

drmeloy commented Jan 18, 2022

@TkDodo
Apologies for the split comments, I hit submit a bit early.

As a follow-up I put a console.log inside the QueryCache's onError:

  const [queryClient] = useState(() => new QueryClient({
    queryCache: new QueryCache({
      onError: (error) => {
        console.log('ERROR: ', error);
        addErrorToast(error);
      },
    }),
  }));

Of the two error toasts that are being generated I'm only seeing a console log for the second one (the one I am unable to dismiss)

Copy link

drmeloy commented Jan 18, 2022

@TkDodo I tested this implementation on the onError of the individual queries and the error only pops up once rather than twice.

@TkDodo
Copy link
Owner

TkDodo commented Jan 18, 2022

If you only see the log inside onError once, that means it's only called once. The other error toast must come from somewhere else. A codesandbox reproduce would be best.

Copy link

Ok, so you handle all errors with toast popups, but what if you want to display an alert on specific page within specific component? Then global QueryCache callback is practically unusable if you don't have some global state management?

I am frustrated that community doesn't already have clear consensus about handling all 4 possible states (success, loading, error, empty) that should maybe even be built into framework and not let every developer reinvent it in every app. It's common requirement for every possible app.

@TkDodo
Copy link
Owner

TkDodo commented Mar 13, 2022

Ok, so you handle all errors with toast popups

Not sure this should be the takeaway from the blogpost. I've shown 3 ways of handling errors:

  • in the component itself
  • via ErrorBondaries
  • as toasts via callbacks

And I've stated that I personally prefer ErrorBoundaries for "hard errors" and toasts for background errors.

The problem about clear consensus is that it is mostly a ux question that has to be answered on a case-by-case basis. Some errors are so severe that it is necessary to unmount the whole component tree and only show the error. Other errors, especially once where you already have data to be shown instead, might be less severe so they can be handled differently. Some maybe can even be ignored altogether.

Repository owner locked and limited conversation to collaborators Apr 15, 2022
@TkDodo TkDodo converted this issue into discussion #68 Apr 15, 2022

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests