React: Throwing Error from hook not caught in error boundary

Created on 28 Feb 2019  Â·  19Comments  Â·  Source: facebook/react

This may be more of a question than an issue but is it possible to throw errors in a hook and have them caught in an error boundary? Or will this not work?

Needs More Information

Most helpful comment

React can’t catch them — just like it wouldn’t catch errors from async class methods.

You can, however, do a trick like this:

setState(() => {
  throw new Error('hi')
})

That should do what you want, both in classes and with Hooks.

All 19 comments

What have you tried?

So basically I created a hook that wraps apollos client.. and I tried to throw from that hook. See code below: Note the commented line where i throw

import React, { useState, useContext, useEffect } from 'react';
import { ApolloClientContext } from '../../context/context';

const useQuery = ({ query, variables }) => {

  const client = useContext(ApolloClientContext);

  const [loading, setLoading] = useState(true);
  const [data, setData] = useState();
  const [error, setError] = useState();

  const fetchData = async (params) => {
    setLoading(true);
    setError(undefined);
    try {
      const { data: result } = await client.query(params);
      setData(result);
    }
    catch (e) {
      // THIS IS THE IMPORTANT LINE
      if( e.type === 'some type where i want to throw') { 
          throw e 
      }
      setError(e);
    }
    setLoading(false);
  };

  useEffect( () => {
     console.log('FETCHING DATA');
     fetchData({
       query,
       variables
     });
  }, []);

  return {
    loading,
    data,
    error,
    query: fetchData
  };
};

export default useQuery;

I know its throwing. And I also know that I have an error boundary wrapping it.

Here is intended use case:

import React, { useState } from 'react';
import { useQuery } from '../../hooks';

const ComponentWithHook = ({
  foo
}) => {

  const {
    loading,
    data,
    error
  } = useQuery({
    query: SOME_QUERY,
    variables: { foo },
  });

  return (
    <SomeViewComponent 
      loading={loading}
      data={data}
      error={error}
    />
  );
};

class MyErrorBoundary extends React.Component {

  constructor(props) {
    super(props);
    this.state = {};
  }

  componentDidCatch(error, errorInfo) {
    this.setState({
      error
    });
  }

  render() {

    return (
      {error : <small>Ahh there was an error</small> : <ComponentWithHook foo="bar" />}
    );
  }
}

@joepuzzo
It seems to me that error boundaries do catch errors thrown inside hook https://codesandbox.io/s/24l03vz15j?fontsize=14

Mine is async... could that be the issue?

Oh wait so is yours

@joepuzzo I see.
The issue here maybe is that you are not awaiting this code inside the hook, so react moves on before it should. (please someone correct me if i'm wrong)

  useEffect( () => {
     console.log('FETCHING DATA');
    // this should await here so useEffect awaits for the resolution of the promise
     fetchData({
       query,
       variables
     });
  }, []);

I was able to replicate by not awaiting and throwing the error, which error boundary didn't catch. As soon as I await the function inside useEffect, then error boundary caught it

You're right.. im not awaiting.. however when i add that, react barfs at me saying "Warning: An Effect function must not return anything besides a function, which is used for clean-up."

So I'm assuming this is bad behavior then? And there is a better approach.

Now i don't know if error boundaries not catching errors that happens inside async function called from useEffect is by design or not. It makes sense from what I can tell

React can’t catch them — just like it wouldn’t catch errors from async class methods.

You can, however, do a trick like this:

setState(() => {
  throw new Error('hi')
})

That should do what you want, both in classes and with Hooks.

hmm sneaky but I like it lol

@gaearon The combination of that approach, but using the preferred getDerivedStateFromError instead of componentDidCatch and rendering a child component when the error boundary is triggered causes Uncaught Invariant Violation: Rendered fewer hooks than expected. This may be caused by an accidental early return statement. instead of rendering the fallback UI

See the modified code sandbox: https://codesandbox.io/s/2wwj41v7on

Is there something unsupported about this approach?

Swapping getDerivedStateFromError back to componentDidCatch fixes it, so it seems like a bug

@iwilson-r7 Just encountered the same issue. Interestingly enough, it works correctly in a production build without any hiccups. But it is certainly annoying in development as making a mistake basically throws away everything, not even dev error overlay is shown, just console message which is not helpful at all.

https://reactjs.org/blog/2017/07/26/error-handling-in-react-16.html

Error Boundaries aren't supported in React Hooks yet :(

In case anyone lands here using hooks, here's what I'm using, since we don't have setState. If folks know a better way to propagate the error I'd love to know.

import React, { useReducer } from "react";

const initialState = {};

function reducer(state, action) {
  if (action.type === 'error' && action.error) {
    throw action.error;
  }
}

function Component() {
  const [state, dispatch] = useReducer(reducer, initialState);

  useEffect(() => {
      memoizedCallAPI().then((response) => {
        // handle good response
      }).catch(error => {
        dispatch({ type: 'error', error });
      });
  }

Update

Actually we can just do this:

  const [/* state */, setState] = useState();
  setState(() => {
    throw error;
  })

Hi @techieshark , what do you mean by your update? Are you replacing useReducer to useState to throw errors?

@timothysantos originally i incorrectly thought I could only use the useReducer, but later realized useState works as well. Both seem to work, but useState alone seemed simpler if the reducer / dispatch functionality is not also needed.

So in the end, with hooks, my full code looked like:

function Component() {
  const [/* state */, setState] = useState();

  useEffect(() => {
      memoizedCallAPI().then((response) => {
        // handle good response
      }).catch(error => {
        setState(() => {
         throw error;
         })
      });
  }

I hope that clarifies?

Does anyone know how to stop the error stack showing up in the UI?

@techieshark
@timothysantos

Was this page helpful?
0 / 5 - 0 ratings

Related issues

kocokolo picture kocokolo  Â·  3Comments

Prinzhorn picture Prinzhorn  Â·  3Comments

huxiaoqi567 picture huxiaoqi567  Â·  3Comments

zpao picture zpao  Â·  3Comments

varghesep picture varghesep  Â·  3Comments