Error Handling

The React-query useErrorBoundary config property is set by default on the queryClient instance, making sure errors are thrown in the render phase and propagate to the nearest error boundary. A global "catch-all" error boundary has been placed at root level of the component hierarchy, making sure all errors are handled by giving users the option to either cancel or retry a specific action.

App.tsx

const App = () => {
...
const handleErrorReset = (args: any) => {
queryClient.clear();
setNavigationInitialState(args?.retry ? navigationState.current : undefined);
};
return (
<ErrorBoundary FallbackComponent={StateError} onReset={handleErrorReset}>
<NavigationContainer
initialState={navigationInitialState}
onStateChange={state => { navigationState.current = state }}
>
...
</NavigationContainer>
</ErrorBoundary>
);
};
}

Error flow#

As mentioned above, the errorBoundaryin App.tsx handles all errors thrown by react-query or React components. Centralized error handling means developers do not have to worry about handling errors inside React components or queries and mutations. Whenever an error occurs when fetching data, React Query throws an error, which is then catched by the error boundary. Whithin each error response returned by the server is an error code used to generate the correct error message presented to user.

  1. Client requests data with useQuery or performs a mutation with useMutation
  2. Server returns an error
  3. React query throws an error
  4. Error boundary catches the error
  5. Snapshot navigation state
  6. The entire component tree is detached, and the error screen is presented
  7. The error code is extracted from the server response, and presented on the error screen

The user has several options in order to fix the problem:

User clicks "try again"

  1. Client resets the error boundary and clears the react query cache
  2. Client resets to the previous navigation state by using the stored snapshot
  3. The last screen is then rendered, and the data is refetched

User clicks "cancel" or "go back"

  1. Client resets the error boundary and clears the react query cache
  2. Client clears all navigation state
  3. The user is navigated back to the home screen

Error response#

Errors coming from the server contains an object in the response body with properties like status code, message, error type and more. If the server does not have a predefined description of the error, then the returned response body is empty.

ErrorResponse type definition (errorResponse.ts)

export type ErrorResponse = Error & {
body?: {
message: string;
error: string;
status: number;
data?: { [k: string]: string };
};
};

Translations and error messages#

By storing error messages presented to the users on the client, instead of relying on the server, we can make sure they are translated based on the selected language. The error key used to display the correct error message is extracted from the error response. If the serves does not respond with a error code, then a generic error message is displayed to the user.

Error codes and translation tokens (errorMessages.ts):

Error keyTranslation tokens
TIMESLOT_LOCK_DEADLINE_SURPASSEDerror.timeslot_lock_deadline_surpassed.title
error.timeslot_lock_deadline_surpassed.description
GENERICerror.generic.title
error.generic.description

Local errors#

It is of course possible to bypass the global error handling system, and instead handle errors locally if needed. Sometimes it is necessary to display an inline error to the users on the same screen that resulted in an error. One approach is to perform the necessary actions and state management inside the onError callback returned by the query or mutation itself.

const { data } = useQuery({
queryKey: ['foo'],
queryFn: fetchFoo,
useErrorBoundary: false,
onError: (error) => toast.error(`Something went wrong: ${error.message}`),
});

Another approach is to use the error property returned by a query:

const { data, error } = useQuery({
queryKey: ['foo'],
queryFn: fetchFoo,
useErrorBoundary: false,
});
if (error) {
// Do something
}

It is also possible to place the ErrorBoundary closer to the component in the component tree.

const ParentComponent = () => (
<ErrorBoundary FallbackComponent={StateError} inline>
<ChildThatThrows />
</ErrorBoundary>
);

Limitations#

These are the limitations when using an error boundary, i.e. will not catch an error from:

  • Event handlers
  • Asynchronous code (ie. setTimeout)
  • Errors thrown in the error boundary itself (only its children)
me

Previous

Dialogue Chat

Next

External Modules

me