apollo-client: ApolloClient onError doesn't catch mutation errors anymore

Here is my client configuration using Apollo Boost :

new ApolloClient({
  uri: `${window.location.origin}/api`,
  cache: new InMemoryCache(),
  onError({ graphQLErrors, networkError }) {
    if(graphQLErrors)
      graphQLErrors.forEach(error => notification.error({
        message: 'Error',
        description: error.message
      }))
    if(networkError)
      notification.error({
        message: 'Network Error',
        description: `A network error has occurred. Please check out your connection.`
      })
  },
  request(operation) {
    const currentUser = readStore('currentUser')
    currentUser && operation.setContext({
      headers: { authorization: currentUser.token }
    })
  }
})

Intended outcome: Before the bug, the onError callback used to catch errors just fine.

Actual outcome: Errors are said to remain uncaught and cause apps (using CRA in my case) to crash.

How to reproduce the issue: Try switching to the latest version of apollo-boost and just throw a Graphql error on the server.

Versions React : 16.12.0 Apollo-boost : 0.4.7

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 47
  • Comments: 48 (7 by maintainers)

Commits related to this issue

Most upvoted comments

Any update on this? I have a pretty large app that relied on this pattern to minimize the amount of try/catch and .catch that was needed. Made for a super clean code base knowing I could rely on apollo-link-error when needed.

Not sure what changed, guess I need to look at my past few commits, but lately all mutation errors throw an unhandled error.

Is this change documented in a change log somewhere?

Any updates on this one? I can’t believe this issue still not addressed yet

Why is there no activity on this issue? It has been around since December 2019, useQuery and useLazyQuery work as expected so it seems like a deal-breaker for me. I don’t want to write try/catch logic around mutations, that’s why I use the hooks so I can respond to changes in the UI without side-effect code.

But…,finally, I resolved by adding catch in promise mutation, for example. You have: const [login] = useMutation(LOGIN)

then when you call mutation by an event

login({
 variables: {
   input: { email, password }
 }
}).catch(err => console.error(err))

Our issue may resolve, but onError issue is still there, maybe because of promising stream is not correctly, I’m not sure.

Hello! Apollo staff here. First of all I want to apologize for the lack of response in this 43 comment, 3 year old issue. I’m sorry that you all had to experience being here. However, I am also glad to see people using this issue as a way to help each other. For instance, I personally appreciate comments likes those of @valentingavran https://github.com/apollographql/apollo-client/issues/5708#issuecomment-824727128, @viiiprock https://github.com/apollographql/apollo-client/issues/5708#issuecomment-569268951 and @Illia-Linevych https://github.com/apollographql/apollo-client/issues/5708#issuecomment-825201861, people who went out of their way to try and help others even after they had solved their own problems. I also appreciate the comment from @kissthom https://github.com/apollographql/apollo-client/issues/5708#issuecomment-588300148 attempting to find the source of the error in what is likely now a very outdated version of the codebase. I can’t respond directly to every comment in this issue, because that would take a lot of time, but I’m glad that this has been a place for people to help each other, even if we are all frustrated when we’re here.

The problem, as best I understand it, is that the useMutation() hook, in earlier versions of Apollo Client, threw unhandled errors or promise rejections, and that this was causing problems for developers, especially those who use tools like react-dev-server, because the developers who created those tools, thought it would be a good developer experience to throw up an overlay any time it detected an error. Spoiler alert, it’s not.

One thing that complicates this issue is that there are different kinds of errors that can be thrown. Network errors, and errors passed to the client from the server, for instance, might bubble up from the link abstraction, which is why people have suggested overriding the flexible link abstraction to ignore the network error.

Other solutions, like adding an onError() callback to the useMutation() hook, have been added in successive versions of Apollo Client, and they can also squash the error. Additionally, the useMutation() execute() function returns a promise, which if not caught, could, in various versions, cause unhandled promise rejections.

In my estimation, I think people in this issue could be talking about any of these sources or mitigations errors. All of these issues have hopefully been remedied in the latest version of Apollo Client (^3.6), and we encourage you to update when you get the chance.

In short, I think the problems in this issue have been addressed by the latest version of Apollo Client, and I encourage you to open new issues, and hopefully issues with code snippets, if you think we have not addressed the problems in this issue.

If you’re just looking for a place to vent about Apollo and GraphQL, I hear you. I feel the same way about a lot of this. Please do not let me closing this issue prevent you from complaining. Go right ahead. Nevertheless, we hope to surprise and delight you with what we got in store for y‘all. 😊 Thank you for engaging with us!

I’m having a similar issue. When the response from a mutation contains an error I get an error that there was an unhandled rejection. This slows development since react-dev-server displays an error overlay that you have to close. Errors from useQuery and useLazyQuery are handled differently, they get caught internally by apollo-client. At the moment I’m doing

const [cb, {data, error}] = useMutation(MUTATION, {onError: () => null})

but that’s not ideal either. I was previously using apollo-boost and this was not an issue.

Found a workaround that worked for me in an old issue comment.

With apollo-link-error return Observable.of(operation); at the end of the error handling logic.

import { onError } from "apollo-link-error";

const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
  if (graphQLErrors)
    graphQLErrors.forEach(({ message }) =>
      console.log(`[GraphQL error]: Message: ${message}`)
      return Observable.of(operation);
    );
  if (networkError) {
    console.log(`[Network error]: ${networkError.message}`);
    return Observable.of(operation);
  }
  return forward(operation);
});

Also experiencing this issue (onError not invoked for mutation errors) on @apollo/client v3.3.14

for me query errors are fine and mutation errors throw uncaught exception, can you confirm if you have the same problem?

To be clear, we believe the original onError regression should have been addressed by PR #9076, which was first released in v3.5.4. This issue probably should have been closed when that PR was released, so at least we could’ve found out sooner if it wasn’t really fixed. We are (as @brainkim said) happy to continue the discussion here and reopen if necessary!

I think it’s going in the right direction @adisrael , but it’s not working for me.

I mean, I see the global log console.log([GraphQL error]..., but shouldn’t it go back also until the mutation? and have result.errors filled?

const result = await CreatePeople({
   variables: { fields, accountId: contratId },
});
console.log(`result.errors`, result.errors);

Thx for your help

also seeing the same problem. @viiiprock thanks for the workaround.

is this considered a bug or an expected behavior? can someone from the apollo team chime in here?

it’s been 2.5 years, and afaict, no one from the apollo team has chimed in.

Maybe it’s time to move away from apollo? 🤔

Was able to find a hack/solution by overriding the default value of errorPolicy specifically for mutations.

Noticed that the line of code that’s throwing these errors occurs at https://github.com/apollographql/apollo-client/blob/v3.5.8/src/core/QueryManager.ts#L238.

So you can simply just set the errorPolicy to a non 'none' value (I opted for 'all') at the client level with defaultOptions. This way you don’t have to try/catch or use .mutation().catch() after each usage since it no longer throws an error, just returns the error result.

e.g:

import { ApolloClient, InMemoryCache } from '@apollo/client';

const cache = new InMemoryCache();

const client = new ApolloClient({
  cache: cache,
  uri: 'http://localhost:4000/',
  defaultOptions: {
    mutate: {
      errorPolicy: 'all',
    },
  },
});

The difference of using 'all' rather than the default 'none' wasn’t problematic since the codebase I work with never relied on data to be undefined, and instead relied on the existence of an error response.

I’m try to fix it with response.errors = undefined but it doesn’t work in any case. I have to add the onError in each query or mutation 😕

Same issue here.

If I had to guess I’d say this line in the catch block makes the trouble.
MutationData.runMutation

  private runMutation = (
    mutationFunctionOptions: MutationFunctionOptions<
      TData,
      TVariables
    > = {} as MutationFunctionOptions<TData, TVariables>
  ) => {
    this.onMutationStart();
    const mutationId = this.generateNewMutationId();

    return this.mutate(mutationFunctionOptions)
      .then((response: FetchResult<TData>) => {
        this.onMutationCompleted(response, mutationId);
        return response;
      })
      .catch((error: ApolloError) => {
        this.onMutationError(error, mutationId);
        if (!this.getOptions().onError) throw error; // !!! I mean this line
      });
  };
// ... more code here...
  private onMutationError(error: ApolloError, mutationId: number) {
    const { onError } = this.getOptions();

    if (this.isMostRecentMutation(mutationId)) {
      this.updateResult({
        loading: false,
        error,
        data: undefined,
        called: true
      });
    }

    if (onError) {
      onError(error); 
    }
  }

Of course I don’t know the whole implementation but it seems to me that in MutationData.onMutationError the errors are already handled pretty well, onError will be called conditionally as it should be so I’m not sure if rethrowing the error is really necessary.

Another workaround is to provide a custom fetch function which transforms the HTTP response code to a 200:

import { ApolloClient } from '@apollo/client'
import { createHttpLink } from '@apollo/client/link/http'

const errorlessFetch = (
  uri: string,
  options: RequestInit,
): Promise<Response> => {
  return new Promise((resolve) => {
    fetch(uri, options).then((response) => {
      // Treat 422s as 200s:
      if (response.status === 422) {
        const { body, ...options } = response
        return resolve(new Response(body, { ...options, status: 200 }))
      }

      return resolve(response)
    })
  })
}

const httpLink = createHttpLink({
  fetch: errorlessFetch,  // <--------------- over-ride fetch()
  uri: 'http://localhost/graphql',
})

export const apiClient = (): ApolloClient<unknown> => {
  return new ApolloClient({
    link: httpLink,
  })
}

Maybe it’s time to move away from apollo? 🤔

I eventually moved to urql. No regrets.

I’m not sure if I understood this issue correctly, but according to the information I found in the documentation, here’s how you should catch the errors. This works for me without any problems. Maybe this will help someone.

import { ApolloClient } from "apollo-client";
import { setContext } from "apollo-link-context";
import { HttpLink } from "apollo-link-http";
import { ApolloLink } from "apollo-link";
import { onError } from "apollo-link-error";
import { InMemoryCache } from "apollo-cache-inmemory";

const httpLink = new HttpLink({
  uri: "http://localhost:10000"
});

const authLink = setContext((_, { headers }) => {
  // return the headers to the context so HTTP link can read them
  const accessToken = localStorage.getItem("accessToken");
  return {
    headers: {
      ...headers,
      authorization: accessToken
    }
  };
});

const cache = new InMemoryCache();

const apolloClient = new ApolloClient({
  link: ApolloLink.from([
    onError(() => {
      console.log("onError called");
      // Handle errors
     // ...
    }),
    authLink.concat(httpLink)
  ]),
  cache,
});

export default apolloClient;

It’s a big Apollo’s fail. Looks like the library has some problem. It has more than 600 open issues and the roadmap for the V3 is from 2018. I don’t know if it will be done. About the onError I “fixed” it with a patch:

type useMutationFnType<TQuery, TVariables> = (
  baseOptions?: MutationHookOptions<TQuery, TVariables>
) => MutationTuple<TQuery, TVariables>;

function useWithErrorMiddleware<TQuery, TVariables>(
  useMutationFn: useMutationFnType<TQuery, TVariables>,
  baseOptions?: MutationHookOptions<TQuery, TVariables>,
) {
  const onError = (error: ApolloError) => {
    if (baseOptions && baseOptions.onError) {
      baseOptions.onError(error);
    }
    return false;
  };
  const options = {
    ...baseOptions,
    onError,
  };
  return useMutationFn(options);
}

So you can use it like. I’m using codegen:

const [updatePasword, { loading, error }] = useWithErrorMiddleware(
    useProfileUpdatePasswordMutation,
    {
      errorPolicy: 'all',
      onCompleted: (data) => {
        if (data) {
          const { message, type } = data.profileUpdatePassword.message;
          notification[type]({ message });
        }
      },
    },
  );

hate this lib and react. bloated and fully opinionated 👎

apollo v3.1.3 confirm the same error

@ryanrhee I am chiming right now let me just read through 40 comments of people talking past each other, one sec.

Was able to find a hack/solution by overriding the default value of errorPolicy specifically for mutations.

Noticed that the line of code that’s throwing these errors occurs at v3.5.8/src/core/QueryManager.ts#L238.

So you can simply just set the errorPolicy to a non 'none' value (I opted for 'all') at the client level with defaultOptions. This way you don’t have to try/catch or use .mutation().catch() after each usage since it no longer throws an error, just returns the error result.

e.g:

import { ApolloClient, InMemoryCache } from '@apollo/client';

const cache = new InMemoryCache();

const client = new ApolloClient({
  cache: cache,
  uri: 'http://localhost:4000/',
  defaultOptions: {
    mutate: {
      errorPolicy: 'all',
    },
  },
});

The difference of using 'all' rather than the default 'none' wasn’t problematic since the codebase I work with never relied on data to be undefined, and instead relied on the existence of an error response.

I tried this, but this way the onError handler is never called

const [mutate, {error}] = useMutation({onError: (errorFromHandler) => console.log("you can't see me", errorFromHandler)})
console.log("this works", error)

Any update regarding this issue?

the only workaround that seems to work for me is to provide onError: () => null as described above

const [exec] = useMutation(myMutation)
handleThing = () => {
  const result = await exec({onError: () => null, ... })
}

confused about how the many apollo-client users work around this? only using onError / onCompleted callbacks? wrapping all mutation promises with try { ... } catch (err) { ... } ?

Still broken. Used code similar to example from docs:

const errorLink = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors)
    graphQLErrors.forEach(({ message, locations, path }) =>
      console.log(
        `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
      )
    );
  if (networkError) console.log(`[Network error]: ${networkError}`);
});

Setting error policy does not work.

The only working way I found is to use catch after client.query():

client.query({ query: SomeTestQuery }).catch((error) => console.log(error))

But catching errors like this (especially if you’re working on a big project) is definitely the way to an asylum.

Hi there. Pinging this thread because I believe I’m running into this issue as well. I’m getting networkError when a mutation errors. Could anyone provide a working alternative, or could Apollo advise us on how we should proceed? Thanks.

Using return Observable.of(operation) results on Missing field 'XXX' while writing result {} error being printed on console, any idea how to solve this?

@krakzk Yep, for sure!

    const errorLink = onError(({graphQLErrors, networkError, response}) => {
        if (graphQLErrors) {
            graphQLErrors.map(({message, extensions, locations, path}) => {
                //  custom error catching logic
            }
            response.errors = undefined
        }
    }

On my end, the “custom error catching logic” do operations depending on extension categories / content of messages, then dispatch an action to my errors reducer. Hope it helps.