apollo-client: Testing Error State of Mutation is throwing Global Error
I want to reopen https://github.com/apollographql/react-apollo/issues/2614 from the old repo as I ran into the exact same Issue today with @apollo/client": "3.2.2
.
I have a component that uses the useMutation hook and renders differently when useMutation returns an error
export const DELETE_DOG_MUTATION = gql`
mutation deleteDog($name: String!) {
deleteDog(name: $name) {
id
name
breed
}
}
`;
export function DeleteButton() {
const [mutate, { loading, error, data }] = useMutation(DELETE_DOG_MUTATION);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error!</p>;
if (data) return <p>Deleted!</p>;
return (
<button onClick={() => mutate({ variables: { name: 'Buck' } })}>
Click me to Delete Buck!
</button>
);
}
I want to Test this behaviour as described in the Docs
it('should show error UI', async () => {
const deleteDog = { name: 'Buck', breed: 'Poodle', id: 1 };
const mocks = [
{
request: {
query: DELETE_DOG_MUTATION,
variables: { name: 'Buck' },
},
error: new Error('aw shucks'),
},
];
const component = renderer.create(
<MockedProvider mocks={mocks} addTypename={false}>
<DeleteButton />
</MockedProvider>,
);
// find the button and simulate a click
const button = component.root.findByType('button');
button.props.onClick(); // fires the mutation
await new Promise(resolve => setTimeout(resolve, 0)); // wait for response
const tree = component.toJSON();
expect(tree.children).toContain('Error!');
});
Intended outcome:
The Test should pass without throwing an error.
Actual outcome:
Passing mocks with an Error to <MockedProvider />
does actually throw a global Error
aw shucks
at new ApolloError (node_modules/@apollo/client/errors/index.js:26:28)
at Object.error (node_modules/@apollo/client/core/QueryManager.js:146:48)
at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
at node_modules/@apollo/client/utilities/observables/iteration.js:4:68
at Array.forEach (<anonymous>)
at iterateObserversSafely (node_modules/@apollo/client/utilities/observables/iteration.js:4:25)
at Object.error (node_modules/@apollo/client/utilities/observables/Concast.js:33:21)
at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
I figured out that when I add an onError
option to useMutation
my test runs as expected but I guess this is just a workaround.
const [mutate, { loading, error, data }] = useMutation(DELETE_DOG_MUTATION, {onError: () => {}});
How to reproduce the issue:
I cant share the code of the application I am working on but I can try to create a Codesandbox if neccessary but I hope my explanation is detailed enough.
Versions System: OS: macOS Mojave 10.14.6 Binaries: Node: 10.16.0 - /usr/local/bin/node npm: 6.11.2 - /usr/local/bin/npm Browsers: Chrome: 86.0.4240.80 Firefox: 81.0.1 Safari: 12.1.2 npmPackages: @apollo/client: ^3.2.2 => 3.2.2 “jest”: “^24.9.0”,
About this issue
- Original URL
- State: open
- Created 4 years ago
- Reactions: 23
- Comments: 19 (5 by maintainers)
This comment managed to get around it using errorPolicy:
You can then target to find the element rendered by
if (error) return <p>Error!</p>;
For me, its a lack of documentation around the handling of errors in general for mutations (via useMutation) which is the issue. I had to go through a laborious trial/error trying to understand the scenarios in which a mutation would throw or call the
onError
option.onError
option is present via theuseMutation
options or the mutation function options then it will NOT throw (oddly if anonError
option exists in both places, they BOTH get called instead of the mutation function option overriding theuseMutation
option as it does for a query).onError
option present then the mutation function will throw.This is different to how queries work, where they don’t throw at all and instead pass errors back either through the return value of a lazy query, or the error property of a greedy query. I would expect a lazy query and a mutation to behave in a similar way as they have a similar interface.
It is different again if you use the
client
directly in which queries and mutations throw. Personally I would like to see mutations (viauseMutation
) not throw if aonError
option is not present as that would align them with the query behaviour.Just got bitten by this … wasted a couple of hours. If there’s no appetite to change behaviour by the Apollo team, then this behaviour really needs to be highlighted in the documentation. I guess my expectation was any errors would be passed back in the
error
property, without ALSO throwing.Seems the best way to handle this is by adding a empty
onError
handler in the options in anyuseQuery
oruseMutation
- but it would be great if there was an option to suppress the error also being thrown and just handle it in theerror
property if needed.Oh ya that will definitely work as well. I just wanted to point out that there is a difference between the 2 error properties and they aren’t interchangeable since they impact the behavior in different ways.
Glad this solution works well for you though!
+1 Bumping this! Issue has been open for almost 18 months.
I’ve been struck with the same issue just now and while this seems obvious now I have to admit that documentation isn’t clear about this, so here’s what’s happening: even though the
error
attribute is populated, themutate
function is still throwing an error (eitherError
orGraphQLError
depending on the nature).That being said, in this example:
mutate
is called in a lambda but the exception is never caught, so it is bubbling up, ending in the test failing as it is catching the error that’s thrown. The solution is to swallow the error thrown by the mutation function one way or another. Even though you can get the error in the response object, the error is still being thrown and thus, it has to be taken care of.Documentation while correct does not give an example that can pass tests.
TLDR; in order to avoid your tests failing, you have to use the mutation in an
async/await
function, wrappedtry/catch
, or simply use thePromise
API by adding a.catch()
method to it.