24.5 Error Boundaries in TypeScript Applications
Right, error boundaries. You’ve probably been told they’re React’s magic catch-all for errors in your component tree. And you’d be right, but also, like most magic, it’s mostly clever trickery with a few important limitations. I’m here to demystify it. The core idea is simple: a component that acts like a giant try/catch block for its entire subtree. When something in that tree blows up, this component catches the error, logs it, and displays a friendly “something went wrong” UI instead of letting the entire app vanish into the void. It’s your application’s immune system.
Here’s the crucial bit you need to internalize right now: Error Boundaries are class components. I know, I know. It feels like finding a VHS tape in a streaming service. The React team has been very clear: they have no plans to add hooks for this because the class component lifecycle (getDerivedStateFromError and componentDidCatch) is fundamentally what makes it work. So, for this one specific job, we have to dust off the class keyword. Don’t worry, it’s not a trap; it’s just the right tool for this particular job.
The Anatomy of an Error Boundary
Let’s build one from scratch. You’ll see it’s not complicated once you know the two special methods involved.
// ErrorBoundary.tsx
import React, { ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode; // A component to render if we catch an error
}
interface State {
hasError: boolean;
error?: Error; // Optional, but great for logging
}
class ErrorBoundary extends React.Component<Props, State> {
public state: State = {
hasError: false
};
// This lifecycle method is used to update the state *after* an error is thrown.
// It's what renders the fallback UI.
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
// This lifecycle method is for side effects, like logging the error to your service.
// This is your "oh crap" button. Press it to send the error details to Sentry, LogRocket, etc.
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Uncaught error:', error, errorInfo);
// Your logging service integration would go here:
// logErrorToService(error, errorInfo.componentStack);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return this.props.fallback || (
<div>
<h2>Something went sideways.</h2>
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.error && this.state.error.toString()}
</details>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
Now, to use it, you just wrap it around any part of your tree that you think might be fragile.
// App.tsx
import ErrorBoundary from './ErrorBoundary';
import RiskyComponent from './RiskyComponent';
function App() {
return (
<div>
<Header /> {/* This will be fine even if RiskyComponent explodes */}
<ErrorBoundary fallback={<div>Couldn't load the user profile. Bummer.</div>}>
<RiskyComponent /> {/* This might blow up during rendering */}
</ErrorBoundary>
<Footer /> {/* This will also be fine */}
</div>
);
}
What They Can and (More Importantly) Can’t Catch
This is where most developers get tripped up. Error Boundaries are not omnipotent. They are only triggered during React lifecycle methods. Let’s break that down.
They Catch:
- Errors in the
rendermethod of a child component. - Errors in the constructor of a child component.
- Errors in lifecycle methods (
componentDidUpdate,shouldComponentUpdate, etc.) of a child component.
They Do NOT Catch:
- Event handlers. This is the big one. If your
onClickcallback throws an error, that’s a plain old JavaScript error. You need a regulartry/catchinside your event handler. The Error Boundary won’t see it because it happens outside of the rendering process. - Asynchronous code.
setTimeout,setInterval,fetch().then(),async/await— all of these happen outside the React lifecycle. You must handle those errors yourself. - Errors thrown in the Error Boundary itself. Obviously. (Don’t make an Error Boundary that throws errors. It’s a bad look).
- Errors in server-side rendering (SSR). The semantics are different there.
Strategic Placement and the Granularity Trade-Off
Where you put these things is an architectural decision. Do you wrap your entire app? Sure, that’s a good start. It’s better than a blank white screen. But it’s a nuclear option. A single error in a minor widget nukes the entire app experience.
The smarter play is to be granular. Wrap individual feature sections, complex widgets, or any third-party component you don’t fully trust. This strategy contains the failure. The user might lose their sidebar, but they can still interact with the main content. It’s the difference of a single circuit breaker in your house versus one giant breaker for the whole city.
// A more resilient app structure
function App() {
return (
<div>
<ErrorBoundary fallback={<SkeletonHeader />}>
<Header /> {/* If the header fails, we show a skeleton */}
</ErrorBoundary>
<main>
<ErrorBoundary fallback={<div>The main content is having a moment.</div>}>
<ProductPage /> {/* If this fails, the nav and sidebar stay */}
</ErrorBoundary>
</main>
<ErrorBoundary fallback={null}> {/* If the footer fails, just hide it */}
<Footer />
</ErrorBoundary>
</div>
);
}
The Pit of Success: Logging and Recovery
An Error Boundary that just shows a message is only half done. The componentDidCatch method is your hook to log that error to whatever monitoring service you use. This is non-negotiable for a production app. You need to know what’s breaking out in the wild.
Also, consider giving the user a way to recover. A simple “try again” button that resets the Error Boundary’s state can be a lifesaver. You can implement this by using a key prop on the boundary or by passing a reset function via context.
// Inside your ErrorBoundary's state
interface State {
hasError: boolean;
error?: Error;
}
// Inside the fallback UI
<div>
<h2>Something went wrong.</h2>
<button onClick={() => this.setState({ hasError: false })}>Try Again</button>
</div>
It’s not perfect, but it’s often enough to get the user back on their feet without a full page refresh. And that, my friend, feels a lot like magic they’ll actually appreciate.