Handling errors with react-apollo useMutation hook
I have been trying to get my head around this problem but haven't found a strong answer to it. I am trying to execute a login mutation using the useMutation
hook.
TLDR; I want to know what exactly is the difference between the onError
passed in options
and error
given to me by the useMutation
Here's my code snippet
const [login, { data, loading, error }] = useMutation(LOGIN_QUERY, {
variables: {
email,
password
},
onError(err) {
console.log(err);
},
});
On the server-side, I have a preset/hardcoded email used for login and I am not using Apollo or any other client. In the resolver of this Login Mutation, I simply throw an error if the email is not same using
throw new Error('Invalid Email');
Now I want to handle this error on the client-side (React). But my concern is that if I use the 'error' returned from the useMutation
hook and try to show the error in this way
render() {
...
{error && <div> Error occurred </div>}
...
}
the error is updated in the UI but then immediately React shows me a screen with:
Unhandled Rejection (Error): Graphql error: My-custom-error-message
But, if I use onError
passed in options
to useMutate
function, then it doesn't show me this screen and I can do whatever I want with the error.
I want to know what exactly is the difference between the onError
passed in options
and error
given to me by the useMutation
and why does React show me that error screen when onError
is not used.
Thanks!
Apollo exposes two kinds of errors through its API: GraphQL errors, which are returned as part of the response as errors
, alongside data
, and network errors which occur when a request fails. A network error will occur when a server can't be reached or if the response status is anything other than 200 -- queries that have errors
in the response can still have a status of 200. But an invalid query, for example, will result in a 400 status and a network error in Apollo Client.
Apollo Client actually provides four different ways to handle mutation errors:
1.) Calling the mutate
function returned by the hook returns a Promise. If the request is successful, the Promise will resolve to a response object that includes the data
returned by the server. If the request fails, the Promise will reject with the error. This is why you see an "Unhandled Rejection" message in the console -- you need to handle the rejected Promise.
login()
.then(({ data }) => {
// you can do something with the response here
})
.catch(e => {
// you can do something with the error here
})
or with async/await syntax:
try {
const { data } = await login()
} catch (e) {
// do something with the error here
}
By default, the Promise will reject on either GraphQL errors or network errors. By setting the errorPolicy to ignore
or all
, though, the Promise will only reject on network errors. In this case, the GraphQL errors will still be accessible through the response object, but the Promise will resolve.
2.) The only exception to the above occurs when you provide an onError
function. In this case, the Promise will always resolve instead of rejecting, but if an error occurs, onError
will be called with the resulting error. The errorPolicy
you set applies here too -- onError
will always be called for network errors but will only be called with GraphQL errors when using the default errorPolicy
of none
. Using onError
is equivalent to catching the rejected Promise -- it just moves the error handler from the call site of the mutate
function to the call site of the hook.
3.) In addition to the mutate
function, the useMutation
hook also returns a result object. This object also exposes any errors encountered when running the mutation. Unlike the error handler functions we wrote above, this error
object represents application state. Both the error
and data
objects exposed this way exist as a convenience. They are equivalent to doing this:
const [mutate] = useMutation(YOUR_MUTATION)
const [data, setData] = useState()
const [error, setError] = useState()
const handleClick = async () => {
try {
const { data } = await mutate()
setData(data)
catch (e) {
setError(e)
}
}
Having error state like this can be useful when you want your UI to reflect the fact there's an error. For example, you might change the color of an element until the mutation runs without an error. Instead of having to write the above boilerplate yourself, you can just use the provided result object.
const [mutate, { data, error }] = useMutation(YOUR_MUTATION)
NOTE: While you can use the exposed error state to update your UI, doing so is not a substitute for actually handling the error. You must either provide an onError
callback or catch
the error in order to avoid warnings about an unhandled Promise rejection.
4.) Lastly, you can also use apollo-link-error to add global error handling for your requests. This allows you to, for example, display an error dialog regardless of where in your application the request originated.
Which of these methods you utilize in your application depends heavily on what you're trying to do (global vs local, state vs callback, etc.). Most applications will make use of more than one method of error handling.
const [mutationHandler, { data, loading }] = useMutation(YOUR_MUTATION, {
onError: (err) => {
setError(err);
}
});
With this we can access data with loading status and proper error handling to avoid any error in console / unhandled promise rejection.