We all want our app to be stable, to work perfectly in every edge cases. But the sat reality is we are all humans we all make mistakes, and there is no such things as a bug-free code. no matter how careful we are how many automated tests we write, ther always will be situation when something goes terrible wrong.
Why we should catch errors in React?
Starting from version react 16, an error throw during react lifecycle well cause the entire app to unmount itself if not stopped. Before that component would be preserved on the screen, Now an unfortunate uncaught error in some insignificant part of the ui, or even some external library that you have no control over can destroy the entire page and render an empaty screen for everyone or crash page.
How to catch errors in javascript?
When erros comes to catchin those nasty surprises in regular javascript, the tool are pretty straightforward.
We have our old statement try/catch, which is more or less self-explanatory: try to do stuff, and if teh fail catch the mistake and do something to mitigate it.
try {
// if we are doing something wrong, this might throw an error
doSomething();
} catch (e) {
// if error happened, catch it and do something with it without stopping the app
// like sending this error to some logging service
}
Or, if we’re going with the promises, we have a catch method specifically for them. So if we re-write the previous fetch example with promised-based API, it will look like this:
fetch("https://jsonplaceholder.typicode.com/todos/1")
.then((result) => {
// if a promise is successful, the result will be here
// we can do something useful with it
})
.catch((e) => {
// oh no, the fetch failed! We should do something about it!
});
It is the same concept, just a bit diffectn impementation, so for the rest of ath artical i am just going to use try/cathc syntax for all errors.
Simple try/catch in React
When an error is caught, we need to do something with it, right? So, what exactly can we do, other than logging it somewhere? To Be more precise: what can we do for our user? just leaving them with an empty screen or broken interface is not exacly user friendly.
The most obvious and intuitive answer woul be to render something while we wait for the fix. we can do whaever we want in that catch statement, icluding setting the state. So we can do something like this
const SomeComponent = () => {
const [hasError, setHasError] = React.useState(false);
React.useEffect(() => {
try {
// do something like fetching some data
} catch(e) {
// oh no! the fetch failed, we have no data to render!
setHasError(true);
}
})
// something happened during fetch, lets render some nice error screen
if (hasError) return <SomeErrorScreen />
// all's good, data is here, let's render it
return <SomeComponentContent {...datasomething} />
}
We are trying to send a fetch request, if it falis setting the error stae, and if teh error state is true, then we render an error screen with some additional into for users, like a support contact number.
This approch is pretty starightforward and works great for simple, predictable, and narrow use cases like catching a failed fetch request.
But if you want to catch all errors that can happen in a component, you’ll face some challenges and serious limitation.
Frist Limitation : You will have trouble with useEffect hook.
If we wrap useEffect with try/catch, it just won’t work becouse we cant to wrap useEffect Hooks.
try {
React.useEffect(() => {
throw new Error('Hulk smash!');
}, [])
} catch(e) {
// useEffect throws, but this will never be called
}
It is happening becoause useEffect is called asynchronously after render, so from try/catch perspective everything wen successfully. It is the same story as with any promise: if we don’t wait for the result, then javascript will just continue with its business, return to it when the promise is done, and only execute what is inside useEffect. try/catch block well be executed and long gone by then.
In order for erros inside useEffect ot be caught, try/catch sould be placed inside as well:
React.useEffect(() => {
try {
throw new Error('Hulk smash!');
} catch(e) {
// this one will be caught
}
}, [])
Second Limitation: children components. try/catch won’t be able to catch anything that is happening inside children components. you can’t just do this:
const Component = () => {
let child;
try {
child = <Child />
} catch(e) {
// useless for catching errors inside Child component, won't be triggered
}
return child;
}
Or Even this
const Component = () => {
try {
return <Child />
} catch(e) {
// still useless for catching errors inside Child component, won't be triggered
}
}
Third Limitation : Setting state during render is a no-no
If you’re trying to catch errors outside of useEffect and various callbacks (i.e. during component’s render), then dealing with them properly is not that trivial anymore: state updates during render are not allowed.
Simple code like this, for example, will just cause an infinite loop of re-renders, if an error happens:
const Component = () => {
const [hasError, setHasError] = useState(false);
try {
doSomethingComplicated();
} catch(e) {
// don't do that! will cause infinite loop in case of an error
// see codesandbox below with live example
setHasError(true);
}
}
We could, of course, just return the error screen here instead of setting state:
const Component = () => {
try {
doSomethingComplicated();
} catch(e) {
// this allowed
return <SomeErrorScreen />
}
}
But that, as you can imagine, is a bit cumbersome, and will force us to handle errors in the same component differently: state for useEffect and callbacks, and direct return for everything else.
// while it will work, it's super cumbersome and hard to maitain, don't do that
const SomeComponent = () => {
const [hasError, setHasError] = useState(false);
useEffect(() => {
try {
// do something like fetching some data
} catch(e) {
// can't just return in case of errors in useEffect or callbacks
// so have to use state
setHasError(true);
}
})
try {
// do something during render
} catch(e) {
// but here we can't use state, so have to return directly in case of an error
return <SomeErrorScreen />;
}
// and still have to return in case of error state here
if (hasError) return <SomeErrorScreen />
return <SomeComponentContent {...datasomething} />
}
If we rely solely on try/catch
in React, we will either miss most of the errors, or will turn every component into an incomprehensible mess of code that will probably cause errors by itself.
If you liked this article, then please subscribe to our YouTube Channel for useful videos. You can also find us on Twitter and Facebook.
Write a Reply or Comment