useSuspenseQuery and query errors confusion

Hello!

I’m using useSuspenseQuery to suspend my render until the query is done. I’m confused about the fact that useSuspenseQuery seems to return an error (in addition to data). But, if the query fails, that error never seems to actually be returned to me. Instead, the error makes it to a higher-level <ErrorBoundary> component. This is the documented behavior, but I’m left to wonder what the point of the advertised error return is. Is it ever used?
Indeed, in one use case I would have preferred to get the error through this return - for one, because the ErrorBoundary doesn’t really get any information about which query failed out of possibly multiple.

Thanks!

I have discovered that, if I set errorPolicy: "all", I get the error in the error return value on query completion, instead of the error being thrown as an exception. This seems to be the case for useSuspenseQuery, as well as in other situations like client.query(). Are there any docs that explain this? I’m reading Handling operation errors - Apollo GraphQL Docs, and I don’t see mentions of throwing exceptions versus returning errors in relationship to the errorPolicy.

Thanks!

Hey @andreimatei :wave:

Check out the Suspense error handling docs for this documented behavior. There are two key sections that document the throw vs error property behavior. The first sentence in the error handling section states:

By default, both network errors and GraphQL errors are thrown by useSuspenseQuery . These errors are caught and displayed by the closest error boundary.

And this section on “Rendering partial data alongside errors” states:

In some cases, you may want to render partial data alongside an error. To do this, set the errorPolicy option to all . By setting this option, useSuspenseQuery avoids throwing the error and instead sets an error property returned by the hook. To ignore errors altogether, set the errorPolicy to ignore . See the errorPolicy documentation for more information.

Is there something that could be more clear on our end to help clarify this behavior?

Thanks, Jerel!

In retrospect, the docs on suspense error handling are indeed fine; thanks for spelling them out.
The docs on useQuery also seem fine.

One thing that threw me (ha!) though is the behavior of client.Query() (with client = useApolloClient()). As far as I can tell, client.Query()behaves likeuseSuspenseQuery(), respecting errorPolicyand throwing if the policy isNone, and returning the error otherwise. I did not see this documented. I am also a bit confused about the errorsvserrorfields inApolloQueryResult`; consider if there’s sufficient docs about them.


If you don’t mind, I also have a question about the interaction of errors and caching. It seems to me that error responses are cached (at least for “resolver errors”; not sure about network errors), and the cached data is returned by subsequent queries (at least when using the default fetchPolicy). Is there any way to avoid caching errors? I can’t find a discussion about this in the docs. In an old version of the docs I did find a note about how errors are cached if errorPolicy = "all" is used, and not otherwise. Is that still current? If so, is there still a way to get the error returning behavior of errorPolicy = "all", but not the error caching behavior?

Thanks!

As far as I can tell, client.Query()behaves like useSuspenseQuery(), respecting errorPolicyand throwing if the policy is None, and returning the error otherwise. I did not see this documented

Ah ya what you’re seeing here is the difference in our “core” API and the “React” API. client.query() is UI framework agnostic, and because it returns a promise which you need to await yourself, rejecting on errors here make sense. Totally agree with you though that our documentation could be better in this regard. The closest thing we have to spelling this out is the client.query() API doc, which states:

This resolves a single query according to the options specified and returns a Promise which is either resolved with the resulting data or rejected with an error.

This is hard to find though. At some point in the future, we’d like to revamp our docs to provide more documentation on our core API first, while moving some of the React bits later. Our docs make it seem like Apollo Client is a React library since it provides the basis of most of the client behavior through the React API, but this isn’t quite accurate as the core API provides pretty rich behavior on its own.

We’ll do our best to see if we can find some stop-gap solutions for now until we get our docs revamp, and this is definitely one of them. Sorry for the confusion here!

I am also a bit confused about the errorsvs errorfields in ApolloQueryResult`; consider if there’s sufficient docs about them.

To be honest, I’m not entirely sure of the history here and why this design choice was made. I believe errors is a shortcut to error.graphqlErrors, but its not entirely obvious to me why we can’t just ask our users to access this via the error field.

This is actually why useSuspenseQuery only exposes error (when you have the right errorPolicy set), to try and avoid this confusion. I’d love to see if we can remove the error/errors distinction and make this a little nicer in a future version, but we’ll need to do this in a major version as this would be a breaking change.

It seems to me that error responses are cached (at least for “resolver errors”; not sure about network errors)

Could you describe to me what you mean by “resolver errors” here?

Errors should not be cached at all by the client, so if you’re seeing this, there must be a bug somewhere. What version of Apollo Client are you using?

Sorry for coming back to this old thread about error handling. I now have one more thing that confuses me, and appears to be undocumented.

Here’s what I’m seeing:
I’m using useMutation, and running the mutation generates a network error.
If I don’t specify onError when performing the mutation call, the network error is thrown regarless of the error policy that I set on the call. However, if I do set an onError callback on the mutation call, then the error is returned in the FetchResult that resolves the promise returned by the mutation function. Is that true?

As a by the way, Mutations in Apollo Client - Apollo GraphQL Docs says about onError:

A callback function that’s called when the mutation encounters one or more errors (unless errorPolicy is ignore ).

The part about unless “errorPolicy is ignore” seems to not be true. The callback seems to be called even when the policy is ignore.


Separately, there was another thing that I found quite confusing. I believe I understand what’s going on and it makes sense (in so far as the React programming model makes sense), but perhaps there’s an opportunity to call things out in documentation:
I was confused about the MutationResult return value of useMutation, and it’s relationship with the FetchResult value of the promise returned by the mutation function.
Mutations in Apollo Client - Apollo GraphQL Docs says about the “Mutate function”:

The mutate function returns a promise that fulfills with your mutation result.

This is arguably inaccurate. It seems to suggest that the promise will resolve with MutationResult, but that’s not true. The promise resolves with a different FetchResult; the FetchResult structure does not appear to be documented.

What was more confusing to me is when exactly the MutationResult is populated in response to the last call to the mutation function. If you’re in an async function and you await the promise returned by the mutation function, and then try to reference the MutationResult returned by the useMutation, you’ll notice that the MutationResult has not been populated once (or rather, you’re still referencing the MutationResult from the last render). In other words, the FetchResult that you have now will only be reflected in the MutationResult on the next render. Perhaps this could be hinted at in the docs.

Hey @andreimatei :wave:

Apologies for responding so late! I didn’t see you had responded until today!

If I don’t specify onError when performing the mutation call, the network error is thrown regarless of the error policy that I set on the call.

I agree this is super confusing. This is again one of those things that I don’t have a lot of context of the history of this design decision, but agree that its unintuitive. Unfortunately its a breaking change to “fix” in v3.x so we’ll have to wait for a future major. I’ll go on a limb and say this is very likely to change in a future major as we’ve had this complaint quite a few times.

The part about unless “errorPolicy is ignore ” seems to not be true. The callback seems to be called even when the policy is ignore .

This one is a bit interesting and where the distinction between network errors and GraphQL errors comes into play. As the documentation for the ignore error policy states:

graphQLErrors are ignored (error.graphQLErrors is not populated), and any returned data is cached and rendered as if no errors occurred.

The key thing here is that this only applies to GraphQL errors, NOT network errors, so the behavior you’re seeing reflects that.

Here are a couple tests that demonstrate the difference in behaviors between the two types of errors:

Apologies for the confusion here! Hope this clears that up.


Thats a good callout on the documentation. Unfortunately I think this is one of those things where the words weren’t necessarily meant to correlate to the type names so “mutation result” is used as a more generic term to mean “the thing you get back when calling the mutation” instead of "the thing that is a MutationResult type.

If you’re in an async function and you await the promise returned by the mutation function, and then try to reference the MutationResult returned by the useMutation , you’ll notice that the MutationResult has not been populated once

This makes sense purely from a JavaScript point of view and has to do with closures. A React render is just React calling your component function again. In order for you to reference the new values in your component, React would have needed to call your component function again to create a new closure scope.

Here is an example to illustrate the issue. Try this in your browser console:

let outerName = "Mozilla";
function makeFunc() {
  const name = outerName;
  function displayName() {
    console.log(name);
  }
  return displayName;
}

const myFunc = makeFunc();
outerName = "Changed";
myFunc();

You might think that “Changed” would be logged, but instead you get “Mozilla”! In order for you to get “Changed”, you’d need to re-run the makeFunc, then execute the resulting value.


I’ll see if there is anything we can do to clear up the verbiage between the types and the language used for values you’d expect to get back from awaiting your function, but we’ll likely leave out the base JS stuff since its just JS behavior and less about how the client works.

Hope this helps!

Thanks for the comprehensive responses!

I’ll see if there is anything we can do to clear up the verbiage between the types and the language used for values you’d expect to get back from await ing your function, but we’ll likely leave out the base JS stuff since its just JS behavior and less about how the client works.

I think the root of my general confusion stemmed from not really understanding when I’m supposed to use useMutation versus client.mutate(). I now believe that useMutation should be used strictly when you need the data returned by a mutation for the page rendering (as opposed to needing the side-effects of the mutation, for example its refetchQueries to trigger a rerender). When you don’t need the data returned by the mutation, I think you’re better off using client.mutate() as that results in a simpler mental model → it doesn’t trigger a re-render. Perhaps consider whether there’s an opportunity for the docs to provide more guidance on the topic.

Thanks again!

Thats fair!

You’re free to use whichever tool suits your needs. useMutation uses client.mutate() under the hood. Think of useMutation as the React bindings for that core API.

If you don’t need/care about the React-y parts of the useMutation hook, there is no reason you couldn’t use client.mutate directly. Most people just find it easier to work with useMutation since it has those bindings (especially for handling loading states, since useMutation does that for you without the need to introduce a useState yourself).

Perhaps consider whether there’s an opportunity for the docs to provide more guidance on the topic.

At some point we are planning to rewrite a good chunk of our docs to show off the core API in much more detail. Right now our docs are almost exclusively reflected in terms of React. While many of our users are React users, we know this is a missed opportunity to provide education on how the core APIs themselves work for those that aren’t using React as their UI framework of choice. Its a large undertaking though, so it will be some time before we are able to complete this work. Know that we are aware of it and are planning this!

Feel free to follow these issues for updates:

When you don’t need the data returned by the mutation, I think you’re better off using client.mutate()

You’re definitely entitled to your opinions here :slightly_smiling_face: , but we’ll likely leave this kind of opinion out of the docs unless there are specific use cases where we’d prefer you use one over the other. I’ll refer again to the fact that I think one of the core problems here is the lack of documentation on our core API.

Appreciate the feedback here though! Definitely helpful to understand where we have gaps. Thanks!