Well, I had quite a journey today and I would like to share it with you. I'm – like most of you – a huge fan of GraphQL and the Apollo Stack. Those two technologies + React: declarative rendering + declarative data fetching â¤ï¸ - Is there anything that would make a dev happier? I guess there are many things, but anyways. One thing that bothered me a lot today was handling errors globally. 😤
Imagine the following scenario: An unexpected error occurred. Something really bad. The UI can't and shouldn't recover from that state. You would love to display a completely different UI which informs the user about that case. The Apollo client, or the react-apollo
binding to be precisely, is pretty good when it comes to handling occurred errors on a local level. Something in the vein of: You have a component that "binds" to a GraphQL query and whenever an error occurred you will display something different within that component:
import { compose } from "recompose";
import { graphql, gql } from "react-apollo";
import { ErrorHandler } from "./components";
const NewsList = compose(
graphql(gql`
query news {
id
name
}
`)
)(({ data }) =>
<div>
{data.loading ? (
<span>Loading ...</span>
) : data.errors ? (
<ErrorHandler errors={data.errors} />
) : (
<ul>
data.news.map(entry => <li key={entry.id}>{entry.name}</li>)
</ul>
)}
</div>
);
There is nothing wrong with that approach, except that it doesn't fulfil our aspired scenario in which we want to display an UI the user can't "escape" from. How can we achieve that then?
Afterwares to the rescue!
The Apollo Client comes with a mechanism called Afterware. An Afterware
gives you the possibility to hook you right into the network layer of the Apollo client. It is a function that gets executed whenever a response comes from the server and gets processed by the client. An example:
// graphql/index.js
import ApolloClient, { createNetworkInterface } from "react-apollo";
const createClient = ({ endpointUri: uri }) => {
const networkInterface = createNetworkInterface({ uri });
networkInterface.useAfter([{
applyAfterware({ response }, next) {
// Do something with the response ...
next();
}
}]);
return new ApolloClient({ networkInterface });
};
export { createClient };
Before diving into how to handle the actual error, I would like to complete the example by defining how to create the actual client and use it in your app. The following would be your entry component that bootstraps your application:
// index.js
import { render } from "react-dom";
import { ApolloProvider } from "react-apollo";
import { App } from "./App";
import { createClient } from "./graphql";
const $app = document.getElementById("app");
const client = createClient({
endpointUri: "https://api.graph.cool/..."
});
render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>
, $app);
So that is this. Creating the client and passing it to the ApolloProvider
. Now what? I promised you that we wan't to display a scene which doesn't allow the user to interact with the app. After some tinkering I came to the conclusion that there is a simple solution for that. So here is the dopey idea: Let's pass an additional function to the createClient
function, called onError
which takes an error object and performs a complete new render
on the $app
DOM node. That would allow us to unmount the corrupt UI and render a different component for displaying the respective error case to the user ðŸ¿
First of all: Let's adjust the bootstrapping of the app by defining the onError
function and passing it to the createClient
call:
// index.js
import { render } from "react-dom";
import { ApolloProvider } from "react-apollo";
import { App } from "./App";
import { createClient } from "./graphql";
const $app = document.getElementById("app");
const client = createClient({
endpointUri: "https://api.graph.cool/...",
onError: error => render(
<MyErrorHandler error={error} />
, $app)
});
render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>
, $app);
Afterwards, we have to adjust our Afterware
so that it calls that passed onError
function whenever the server responds with errors:
// graphql/index.js
import ApolloClient, { createNetworkInterface } from "react-apollo";
const createClient = ({ endpointUri: uri, onError }) => {
const networkInterface = createNetworkInterface({ uri });
networkInterface.useAfter([{
applyAfterware({ response }, next) {
if (response.status === 500) {
return onError(new Error(`A fatal error occurred`));
}
next();
}
}]);
return new ApolloClient({ networkInterface });
};
export { createClient };
Wohoo! That's it! From now on, your application would display your <MyErrorHandler />
whenever an error occurred. Mission completed!
Would be great when we could use error boundaries which has been introduced in React 16, but that is not possible due the not "throwing nature" of the Apollo client (which is a good thing when you want to have fine-grained error handling capabilities).
That is it from me for now. Hope you enjoyed the ride and maybe this approach is also useful for you :)
Happy coding!
Top comments (9)
And what about the 200 HTTP Status code errors like this one: Unhandled (in react-apollo) Error: GraphQL error: Not authorized for Query.myQuery at new ApolloError ?
You can handle those via the
afterware
as well. An alternative would be to use theerror
prop which will be passed to your component. Both will work :)But you can't because it's a promise and you don't have yet data.errors.
Hm, can you explain your situation a little bit more? How does the response from your GraphQL look like? Is it stated as an error?
If I first use this code:
...
applyAfterware({ response }, next) {
console.log(response)
...
I have this:
Response {type: "cors", url: "localhost:8080/api", redirected: false, status: 200, ok: true, …}
body: ReadableStream
bodyUsed: true
headers: Headers
ok: true
redirected: false
status: 200
statusText: "OK"
type: "cors"
url: "localhost:8080/api"
proto: Response
I can't read body.
So I found this:
github.com/apollographql/apollo-cl...
and now I'm using:
...
const handleErrors = ({ response }, next) => {
// clone response so we can turn it into json independently
const res = response.clone()
...
And now I can use res.
But what I don't knowis why .clone()? Because response is a response?
After all I need to destroy res? How?
Interesting idea.
My approach was to display notifications for every error. I've released the core of this as a library: github.com/molindo/react-apollo-ne...
Wohoo, awesome work, Jan!
I think I may have found a slightly more up to date and modern method of doing this. I hijacked useQuery to catch every error thrown: github.com/pmaier983/example-apoll...
For Apollo Client 2.0 this solution needs migration due to createNetworkInterface being obsolete as described at:
apollographql.com/docs/react/recip...