Mastering Error Handling in React: From try/catch to Advanced Error Boundaries
This guide explains why error handling is crucial in React, compares try/catch and Error Boundary approaches, highlights their limitations, and presents Dan Abramov's clever technique for catching asynchronous errors, providing practical code examples and best‑practice recommendations.
This article, translated from "How to handle errors in React: full guide," discusses how to capture and handle errors in React, covering try/catch, Error Boundaries, their pros and cons, and a powerful technique introduced by Dan Abramov.
Why error handling matters in React
Since React 16, any error not caught by an error boundary unmounts the entire component tree, potentially showing a white screen. Even a tiny error in a non‑essential UI part can break the whole page.
JavaScript try/catch
In regular JavaScript you can wrap risky code in a try/catch block:
try {
// if we're doing something wrong, this might throw an error
doSomething();
} catch (e) {
// handle the error, e.g., send it to a logging service
}The same pattern works with async functions:
try {
await fetch('/bla-bla');
} catch (e) {
// handle fetch failure
}When using Promises you can use .then() and .catch():
fetch('/bla-bla')
.then(result => {
// handle successful result
})
.catch(e => {
// handle fetch failure
});try/catch inside React components
During rendering you can catch errors and render a fallback UI:
const SomeComponent = () => {
const [hasError, setHasError] = useState(false);
useEffect(() => {
try {
// do something like fetching data
} catch (e) {
setHasError(true);
}
}, []);
if (hasError) return <SomeErrorScreen />;
return <SomeComponentContent {...datasomething} />;
};Limitation 1: useEffect cannot be wrapped by try/catch
Wrapping useEffect itself in try/catch does not catch errors because the effect runs asynchronously after the render phase.
try {
useEffect(() => {
throw new Error('Hulk smash!');
}, []);
} catch (e) {
// never called
}Instead, place the try/catch inside the effect:
useEffect(() => {
try {
throw new Error('Hulk smash!');
} catch (e) {
// this one will be caught
}
}, []);Limitation 2: try/catch cannot catch errors in child components
Creating a JSX element ( <Child />) does not execute the component yet, so surrounding it with try/catch never catches errors that occur during the child's render.
Limitation 3: try/catch cannot set state during render
Updating state inside a render‑time try/catch leads to an infinite render loop. The only safe option is to return a fallback UI directly.
const Component = () => {
try {
doSomethingComplicated();
} catch (e) {
return <SomeErrorScreen />; // allowed
}
};Using Error Boundaries
An Error Boundary is a class component that catches JavaScript errors anywhere in its child tree, logs them, and renders a fallback UI.
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
log(error, errorInfo); // send error to a logging service
}
render() {
if (this.state.hasError) {
return <>Oh no! Epic fail!</>;
}
return this.props.children;
}
}You can provide a custom fallback via a prop:
class ErrorBoundary extends React.Component {
// ...same as before
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
const App = () => (
<ErrorBoundary fallback={<>Oh no! Do something!</>}>
<SomeChildComponent />
<AnotherChildComponent />
</ErrorBoundary>
);Limitations of Error Boundaries
They only catch errors that occur during rendering, lifecycle methods, and constructors. Errors in resolved promises, setTimeout callbacks, or event handlers are not captured.
Catching asynchronous errors with Error Boundaries
Dan Abramov proposes re‑throwing async errors inside a state updater, which triggers the error during the render phase and is therefore caught by an Error Boundary.
const Component = () => {
const [state, setState] = useState();
const onClick = () => {
try {
// something bad happened
} catch (e) {
setState(() => { throw e; }); // re‑throw during state update
}
};
};A reusable hook can encapsulate this pattern:
const useThrowAsyncError = () => {
const [, setState] = useState();
return error => setState(() => { throw error; });
};Usage example:
const Component = () => {
const throwAsyncError = useThrowAsyncError();
useEffect(() => {
fetch('/bla')
.catch(e => {
throwAsyncError(e);
});
}, []);
};Another helper wraps callbacks with the same technique:
const useCallbackWithErrorHandling = callback => {
const [, setState] = useState();
return (...args) => {
try {
callback(...args);
} catch (e) {
setState(() => { throw e; });
}
};
};Existing tool: react-error-boundary
For developers who prefer not to reinvent the wheel, the open‑source library react-error-boundary provides a ready‑made solution.
Conclusion
Effective error handling in React requires combining try/catch for synchronous code, Error Boundaries for lifecycle errors, and the state‑updater re‑throw technique for asynchronous errors. By applying these patterns, you can gracefully handle failures and keep your UI stable.
try/catch does not capture errors inside Hooks like useEffect or child components.
ErrorBoundary catches lifecycle errors but not async or event‑handler errors.
Re‑throwing errors via a state updater lets Error Boundaries capture async errors as well.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
