Error Boundary: Fail Safely with Friendly Fallbacks
Catch runtime errors in a subtree and render a helpful fallback instead of crashing the app.
An error boundary is a React component that catches JavaScript errors in its child tree and renders a fallback UI instead of letting the whole app crash. Even well-tested components throw in production. Without a boundary, one failure takes the entire tree down with it.
You can’t reliably catch render errors yourself. A try/catch inside render logic or event handlers misses lifecycle errors, so scattering them around buys little.
No Boundary, No Mercy
Here’s the unprotected version. Click the button and the thrown error takes the whole app down:
import { useState } from "react";
function BuggyComponent({ shouldError }) {
if (shouldError) throw new Error("Something went wrong!");
return <div>Works until you click the button</div>;
}
function App() {
const [shouldError, setShouldError] = useState(false);
return (
<div>
<h1>Error Boundary Pattern</h1>
<BuggyComponent shouldError={shouldError} />
<button onClick={() => setShouldError(true)}>Cause Error</button>
</div>
);
}
export default App;
Wrap the Risky Subtree
The usual answer is a library like react-error-boundary, but a boundary is small enough to show by hand. getDerivedStateFromError catches the error, componentDidCatch is your logging hook, and resetErrorBoundary lets the user try again:
import { useState, Component } from "react";
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error("Error caught:", error, errorInfo);
}
resetErrorBoundary = () => {
this.setState({ hasError: false, error: null });
if (this.props.onReset) {
this.props.onReset();
}
};
render() {
if (this.state.hasError) {
return this.props.FallbackComponent ? (
this.props.FallbackComponent({
error: this.state.error,
resetErrorBoundary: this.resetErrorBoundary
})
) : (
<div role="alert">
<p>Something went wrong!</p>
<button onClick={this.resetErrorBoundary}>Reset</button>
</div>
);
}
return this.props.children;
}
}
function FallbackComponent({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Something went wrong:</p>
<p>{error.message}</p>
<button onClick={resetErrorBoundary}>Reset</button>
</div>
);
}
function BuggyComponent({ shouldError }) {
if (shouldError) throw new Error("Something went wrong!");
return <div>Works until you click the button</div>;
}
function App() {
const [shouldError, setShouldError] = useState(false);
return (
<div>
<h1>Error Boundary Pattern</h1>
<ErrorBoundary
FallbackComponent={FallbackComponent}
onReset={() => setShouldError(false)}
>
<BuggyComponent shouldError={shouldError} />
<button onClick={() => setShouldError(true)}>Cause Error</button>
</ErrorBoundary>
</div>
);
}
export default App;
The setup is a FallbackComponent that takes error and resetErrorBoundary, the boundary wrapped around the risky subtree, and an onReset callback that clears whatever state caused the error.
Scope boundaries around feature areas, not the entire app, or one broken widget takes down the whole page anyway. Always pass an onReset that actually clears the error-causing state, and log what componentDidCatch hands you to your monitoring. A fallback that swallows errors silently helps nobody.