Search code examples
reactjserror-handlingapolloreact-error-boundary

Error boundary doesn't catch error from apollo client


I'm trying to catch some errors on the top level, to show the most beautiful error page in the world.

For some reason, I see my nodes disconnected, but Error Boundary never fires.

import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
import ApolloClient, { gql } from 'apollo-boost';

const client = new ApolloClient({ uri: "/graphql" });
const query = gql`{ items { id }}`;

class ErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError(error: any) {
    return { hasError: true };
  }

  render() {
    if (this.state.hasError) {
      return <h1>Had error</h1>
    } else {
      return this.props.children;
    }
  }
}

function App() {
  const [data, setData] = useState<any>(null);

  useEffect(() => {
    client.query({ query })
      .then(res => setData(res))
      .catch(err => { console.warn("Error:", err); throw err; });
  }, []);

  return <pre>Data: {data}</pre>;
}

ReactDOM.render(
  <ErrorBoundary>
      <App />
  </ErrorBoundary>,
  document.getElementById('root')
);

I run this in an empty create-react-app project.

I expect to see <h1>Had error</h1>; I get CRA unhandled error screen.


Solution

  • From the docs

    Error boundaries do not catch errors for:

    • Event handlers (learn more)
    • Asynchronous code (e.g. setTimeout or requestAnimationFrame callbacks)
    • Server side rendering
    • Errors thrown in the error boundary itself (rather than its children)

    Promises are asynchronous, so rejected Promises won't be caught by error boundaries. At the moment, the recommended but somewhat hackish approach is to throw the error inside setState. In functional components, you can use the set function returned by the useState hook in place of setState:

    const [, setState] = useState()
    useEffect(() => {
      client.query({ query })
        .then(res => setData(res))
        .catch(err => {
          console.warn("Error:", err);
          setState(() => {
            throw err;
          });
        });
    }, []);
    

    Throwing inside useEffect would work too, but not inside a then or catch callback of a Promise. You also can't make the useEffect callback an async function because it can't return a Promise. So we're stuck with using setState.

    Also note that there is really no reason to be calling client.query directly, particularly since this code will not rerender your UI if the cache changes. You should use useQuery and the data state already exposed by the hook for you.