Articles

Why React Error Boundaries Aren't Just Try/Catch for Components

Kent C. Dodds
Kent C. Dodds

"Why can't I just use try/catch in my React components?" If you've ever asked this, you're not alone! Let's dig into why React error boundaries exist, how they work, and why they're not just a fancy try/catch block for your UI.

The Problem: Errors Happen (and try/catch Isn't Enough)

No matter how careful you are, things will go wrong in your app. Maybe a network request fails, a typo sneaks in, or a third-party library throws a fit. In plain JavaScript, you might reach for try/catch:


try {
// Something that could error
doSomethingDangerous()
} catch (error) {
// Handle the error
showErrorToUser(error)
}

But in React, things are a bit different. When you write JSX like this:


const element = (
<div>
<h1>Calculator</h1>
<Calculator left={1} operator="+" right={2} />
<Calculator left={1} operator="-" right={2} />
</div>
)

React doesn't actually call your Calculator function when it creates this element. It just creates a description of what should be rendered. The real work happens later, when React walks the tree and calls your components. That's why you can't just do this:


try {
const element = (
<div>
<h1>Calculator</h1>
<Calculator left={1} operator="+" right={2} />
<Calculator left={1} operator="-" right={2} />
</div>
)
} catch (error) {
// This won't catch anything!
}

If you try to wrap your JSX in a try/catch, you'll only catch errors that happen during the creation of those elements—not when React actually renders them. The real errors happen inside your components, during rendering, effects, or event handlers.

You could do this:


function Calculator(props) {
try {
// ...render logic
} catch (error) {
return <div>Oh no! An error occurred!</div>
}
}

But that's a nightmare to maintain. Imagine adding this boilerplate to every single component! Plus, it doesn't help with errors that happen deeper in the tree, or in lifecycle methods, or async code.

Enter: Error Boundaries

React introduced error boundaries to solve this. An error boundary is a special kind of component that catches errors anywhere in its child component tree, logs those errors, and displays a fallback UI instead of crashing the whole app.

Here's a minimal error boundary:


class ErrorBoundary extends React.Component {
state = { error: null }
static getDerivedStateFromError(error) {
return { error }
}
render() {
if (this.state.error) {
return this.props.fallback
}
return this.props.children
}
}

And you use it like this:


<ErrorBoundary fallback={<div>Something went wrong!</div>}>
<MyComponent />
</ErrorBoundary>

Now, if MyComponent or anything it renders throws during rendering, React will show your fallback UI instead of a blank screen.

Right now, only class components can be true error boundaries because they use the special static method getDerivedStateFromError. There's no direct equivalent in function components (yet!). That's why libraries like react-error-boundary exist—they give you a nice API and let you use hooks to interact with error boundaries.

But Wait! What About Async Errors?

Error boundaries only catch errors during rendering, (legacy) lifecycle methods, and constructors. They don't catch errors in event handlers, async code, or effects. For those, you need to surface the error to React yourself.

Here's how you might do it with react-error-boundary:


import { useErrorBoundary } from 'react-error-boundary'
function MyComponent() {
const { showBoundary } = useErrorBoundary()
async function handleClick() {
try {
await doSomethingAsync()
} catch (error) {
showBoundary(error)
}
}
return <button onClick={handleClick}>Do something</button>
}

Localized Error Handling: Like try/catch, But Declarative

You can nest error boundaries anywhere in your component tree, just like you'd nest try/catch blocks in code. This lets you show different fallback UIs for different parts of your app:


function App() {
return (
<div>
<ErrorBoundary fallback={<div>Something went wrong with the list.</div>}>
<List />
</ErrorBoundary>
<ErrorBoundary
fallback={<div>Something went wrong with the details.</div>}
>
<Details />
</ErrorBoundary>
</div>
)
}

Resetting and Recovering from Errors

Sometimes, errors are temporary. Maybe the user lost their internet connection, or a form submission failed. With react-error-boundary, your fallback component can offer a "Try again" button:


function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)
}
const element = (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<MyComponent />
</ErrorBoundary>
)

Logging and Analytics

Error boundaries can also help you log errors for analytics or debugging. Use the onError prop with react-error-boundary:


<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, info) => {
// Send error to your logging service
logErrorToService(error, info)
}}
>
<MyComponent />
</ErrorBoundary>

Conclusion

  • Error boundaries are not just try/catch for React—they're a declarative, component-based way to handle errors in the UI.
  • They only catch errors during rendering, lifecycle methods, and constructors—not in event handlers or async code.
  • Use libraries like react-error-boundary for a modern, ergonomic API.
  • Place error boundaries thoughtfully to give users the best possible experience when things go wrong.

Want to learn more? Check out the Epic React Fundamentals workshop and the react-error-boundary docs for deeper dives and more examples.

Get my free 7-part email course on React!

Delivered straight to your inbox.