Why shouldn't I use catch() to handle errors in React useEffect API calls?

Everything I can find on this seems to link back to this github issue circa 2016. I'll quote verbatim from there since it doesn't appear to have been covered on Stack Overflow before and it explains things pretty thoroughly:


.then(() => {
  this.setState({ loaded: true })
})
.catch(()=> { 
  console.log('Swallowed!') 
});

Your catch() handler is going to catch any error thrown in the then() chain before it, including the one caused by a render() due to a setState() call.

Even if you don't use setState directly, you may have the same problem if some other code you call uses it (for example, a Redux dispatch()).

If you don’t want to catch errors resulting from setState(), and want to only catch network failures (let’s imagine your Promise.resolve() is actually a fetch()), you want to use the second then() argument instead:

componentDidMount() {
  Promise.resolve()
  .then(() => {
      this.setState({ loaded: true })
  }, (err) => {
    console.log('An error occurred (but not in setState!)', err);
  });
}

In this case, unless you catch() later in the chain, the error in render() will be uncaught and, with a good Promise polyfill (or with native Promises in Chrome and maybe other browsers), displayed.


Edit: following the answer from @Martin I went and tested this, and I can confirm that this no longer appears to be a relevant concern. Render errors from setState are not caught in any version of React from v16.0 onwards, and since useState was only introuduced in v16.8, it doesn't seem possible that this could ever have been an issue for hooks.

Here is a codesandbox which demonstrates the original issue in the older versions of React.


It is a copy-and-paste error of the example's author. The first example uses the component state of a class based component: this.setState() causes a synchronous re-render (or at least this was the case with react v15.0 in 2016, not sure if it holds true today). Therefore the warning comment and the use of the second arg to .then(onResolve, onReject) is justified. The second example uses the useState hook of a function component: setState causes no synchronous re-render only an asynchronous re-render. Therefore the warning comment and the use of the second arg to .then(onResolve, onReject) is not justified.

To illustrate this: with useState hooks you can call multiple updaters and they all will only take effect in one re-render.

const [a, setA] = useState();
const [b, setB] = useState();
const [c, setC] = useState();

const updateState = () => {
  setA('foo');
  setB('bar');
  setC('buz');
  // will only cause one single re-render
}

and therefore its perfectly fine to use catch

useEffect(() => {
    fetch("https://api.example.com/items")
      .then(res => res.json())
      .then(
        (result) => {
          setIsLoaded(true);
          setItems(result);
        }
      )
      .catch(
        (error) => {
          setIsLoaded(true);
          setError(error);
        }
      )
  }, [])