How errors can be handled with react-query

Introduction

The aim of this post is to outline the possibilities of handling errors in applications with react-query and present you different approaches to it as well as provide an example of usage.

Agenda

  1. React query
  2. Type guards
  3. Global handling with a query client
  4. Custom wrapper for mutations
  5. Providing an example of usage

React query

Let’s start by checking the documentation on website https://tanstack.com/query/latest/docs/react/overview where we should find a lot of useful information about features of this library. In our example only the mutations will be used for errors handling, because the query api will be rebuilt in next version of react-query. To find more information about it you should check out this post on Dominic blog https://tkdodo.eu/blog/breaking-react-querys-api-on-purpose who is one of maintainers this library.

Type guards

The purpose of a type guard is to check the type of an object, so let's create a type guard for the error model and functions for handling errors.

type ApiError = {
  type: string;
  title: string;
  status: number;
  detail: string;
  instance: string;
};

function isApiErrorResponse(res: any): res is ApiError {
  return (
    res &&
    "type" in res &&
    "title" in res &&
    "status" in res &&
    "detail" in res &&
    "instance" in res
  );
}

export const handleErrorMessage = (error: unknown) => {
  if (!axios.isAxiosError(error)) {
    return "Unknown error";
  }

  if (!error.response) {
    return error.message;
  }

  if (!isApiErrorResponse(error.response.data)) {
    return error.message;
  }

  return error.response.data.detail;
};

export const handleErrorCode = (error: unknown) => {
  if (!axios.isAxiosError(error)) {
    throw Error("Unknown error");
  }

  if (!error.response) {
    return error.status;
  }

  if (!isApiErrorResponse(error.response.data)) {
    return error.response.status;
  }

  return error.response.data.status;
};

In above code we should pay attention to the isApiErrorResponse function which is a type guard. It has a specific syntax according to which a type guard should be created:

  • it must be defined with function keyword
  • its parameter type must be any
  • its parameter type must be predicate (res is ApiError)
  • we must check if all the properties are included in the parameter

Global handling with a query client

Let's create global error handling with using the query client in the App.tsx component

const App = () => {
  const [queryClient] = useState(
    () =>
      new QueryClient({
        mutationCache: new MutationCache({
          onError: (error, _variables, _context, mutation) => {
            if (mutation.options.onError) return;

            const errorMessage = handleErrorMessage(error);
            toast.error(errorMessage);
          },
        }),
      })
  );

  return (
    <QueryClientProvider client={queryClient}>
      // Other elements
    </QueryClientProvider>
  );
};

export default App;

In above code we should pay attention to the queryClient definition, because in the documentation the library authors recommended define the queryClient above the component to create only one instance of the queryClient. However, this definition of the queryClient give us the same effect that is recommended by the authors of the react-query library. Another interesting elements is using of the mutationCache where we can define the global error handling, but first condition give us option to handle error differently in our particular mutation.

Upgrade your tech game with us
Contact us

Custom wrapper for mutations

The global error handling with using mutation cache have the dominant flaw which is less possibilities to handle errors, because we can't use any hook e.g. to display an error dialog and we can only rely on toasts. However, we can define our own hook that extends basic definition of the useMutation hook from the library where we have the possibilities to use other hooks to handle errors in a different way. So let's define our custom useMutation hook.

export function useMutationWithErrorHandling<
  TData = unknown,
  TError = unknown,
  TVariables = void,
  TContext = unknown
>(
  mutationFn: MutationFunction<TData, TVariables>,
  options?: Omit<
    UseMutationOptions<TData, TError, TVariables, TContext>,
    "mutationFn"
  >
) {
  const { handleShowDialog } = useDialogContext();

  const showDialog = useCallback(
    (message: string) => {
      handleShowDialog(<CustomDialog message={message} />);
    },
    [handleShowDialog]
  );

  const mutation: UseMutationResult<TData, TError, TVariables, TContext> =
    useMutation(mutationFn, {
      ...options,
      onError: (error) => {
        if (!axios.isAxiosError(error)) throw Error("Unknown error");

        const errorCode = handleErrorCode(error);
        const errorMessage = handleErrorMessage(error);

        switch (errorCode) {
          case 400:
            toast.error(errorMessage);
            break;
          case 404:
          case 500:
            showDialog(errorMessage);
            break;
          default:
            throw Error(errorMessage);
        }
      },
      useErrorBoundary: (error) => {
        const errorCode = handleErrorCode(error);

        switch (errorCode) {
          case 400:
          case 404:
          case 500:
            return false;
          default:
            return true;
        }
      },
    });

  return mutation;
}

In our hook the types are based on the basic definition of the useMutation. Additionally, we can see difference in how the error is handled, as the toast will only be displayed for a 400 error code and a different error code will be handled by the dialog. Also, this hook uses error boundary for other errors that aren't handled in onError method. Finally, I recommend the react-error-boundary library that can handle it.

Example

I would like to present an example of handling errors in difference ways with using the mutation cache, the useMutation hook and our custom useMutation hook, so let's move to the code.

// Mutation Cache and ErrorBoundary
const App = () => {
  const [queryClient] = useState(
    () =>
      new QueryClient({
        mutationCache: new MutationCache({
          onError: (error, _variables, _context, mutation) => {
            if (mutation.options.onError) return;

            const errorMessage = handleErrorMessage(error);
            toast.error(errorMessage);
          },
        }),
      })
  );

  return (
    <QueryClientProvider client={queryClient}>
      <Toaster position="bottom-center" containerClassName="ml-64" />
      <ErrorBoundary fallback={<div>Something went wrong</div>}>
        // Other elements
      </ErrorBoundary>
    </QueryClientProvider>
  );
};

export default App;

// useMutation
export function useMutationNotFoundException() {
  const mutation = useMutation(
    () => commentApi.commentExceptionNotFoundPost(),
    {
      mutationKey: [MutationKeys.NotFoundException],
      onSuccess: () => {},
      onError: () => {
        toast.error("Changed global error handling");
      },
    }
  );
  return mutation;
}

// useMutationWithErrorHandling
export function useMutationServerException() {
  const mutation = useMutationWithErrorHandling(
    () => commentApi.commentExceptionInternalPost(),
    {
      mutationKey: [MutationKeys.ServerException],
      onSuccess: () => {},
    }
  );
  return mutation;
}

To sum up, the presented solution can be an excellent base for expanding error handling when using the react-query library. In addition, I recommend using your own hook for error handling and the error boundary for unhandled errors.

The repositories with examples are available on my github at https://github.com/DamZyl/example-front and https://github.com/DamZyl/example-api.

Related articles

What is enterprise resource planning software
Technology
Business
23/12/24

What is ERP? Your comprehensive software guide

Boost efficiency by integrating processes and managing operations seamlessly with ERP solutions.

Refactor or Rewrite - which is better for your web application?
Technology
13/03/24

Refactoring vs rewrite – which is better for your web application?

Plan to refresh your web app? You are keen to boost efficiency and safety. Check when to choose a code refactor and rewrite.

The journey from theory to practice in software engineering - part 2
Technology
08/03/24

The journey from theory to practice in software engineering - Part 2

Discover surprising applications of computer science theory: job interviews, day-to-day problem-solving, and innovations.

<Our latest articles>

Stay informed with our insightful blog posts

View all posts
Article cover image for Prepare for staff augmentation in 2025 [FREE CHECKLIST]
Business
Outsourcing
10/03/25

Prepare for staff augmentation in 2025 [FREE CHECKLIST]

Learn how to create an evergreen team augmentation strategy and get your copy of our free staff aug checklist!

IT staff augmentation company
Business
Outsourcing
09/03/25

10 red flags when choosing a staff augmentation company

IT staff augmentation is a game of strategy. Spot the 10 red flags early and make the right moves to win.

In-house vs outsourcing 
vs freelance software development
Business
Outsourcing
17/02/25

In-house vs outsourcing vs freelance software development – which option will drive your business forward?

Outsourcing vs in-house vs freelancer. Which one will bolster your business? The key pros and cons.

Upgrade your tech game with us
Contact us

Looking for the IT partner recognised for excellence?

We’ve earned industry-leading awards for delivering top-notch solutions across multiple sectors.

Let’s start your project