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 thethen()
chain before it, including the one caused by arender()
due to asetState()
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 Reduxdispatch()
).If you don’t want to catch errors resulting from
setState()
, and want to only catch network failures (let’s imagine yourPromise.resolve()
is actually afetch()
), you want to use the secondthen()
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 inrender()
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);
}
)
}, [])