DEV Community

André König
André König

Posted on • Edited on

react-apollo: An approach to handle errors globally

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>
);
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
johnunclesam profile image
johnunclesam

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 ?

Collapse
 
andre profile image
André König

You can handle those via the afterware as well. An alternative would be to use the error prop which will be passed to your component. Both will work :)

Collapse
 
johnunclesam profile image
johnunclesam

But you can't because it's a promise and you don't have yet data.errors.

Thread Thread
 
andre profile image
André König

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?

Thread Thread
 
johnunclesam profile image
johnunclesam

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?

Collapse
 
amannn profile image
Jan Amann

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...

Collapse
 
andre profile image
André König

Wohoo, awesome work, Jan!

Collapse
 
pmaier983 profile image
Phillip ED Maier

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...

Collapse
 
keemor profile image
Romek Szwarc

For Apollo Client 2.0 this solution needs migration due to createNetworkInterface being obsolete as described at:

apollographql.com/docs/react/recip...