Failure containment in UI systems
Uncontained render failures propagate upward and can wipe entire UI regions. Error Boundaries provide failure isolation — but async errors require explicit handling.
Resilient systems contain failures instead of letting them cascade.
Break it: render crash without isolation
Trigger a render exception. Without a local boundary, the error bubbles to the app shell and continuity is lost.
- Press "Toggle crash render".
- Observe how the widget crashes and the shell-level fallback replaces the subtree.
- Use "Reset shell" to recover the region.
Why this breaks
Mental model: render errors are synchronous
- Render errors are synchronous — they occur within the render call stack.
- If a component throws during render, React aborts that subtree.
- Without a local Error Boundary, the exception propagates upward until the nearest boundary handles it — often a shell-level boundary.
Detected issue
Without isolation, a render crash replaces the subtree with an app-level fallback. User continuity is destroyed.
Reference (correct mental model)
Correct mental model
Incorrect
Assume success and render risky subtrees without isolation.
// ❌ No isolation
<WidgetThatMayThrow />Correct
Isolate risky subtrees with an Error Boundary and provide a recovery path.
// ✅ Isolation + recovery
<ErrorBoundary fallback={<Fallback />}>
<WidgetThatMayThrow />
</ErrorBoundary>Break it: Error Boundaries do NOT capture async failures
- Press "Throw in setTimeout".
- The error surfaces globally — the boundary does not intercept it.
- Use "Handle async safely" to capture the error at the source.
Async: incorrect vs correct
Incorrect / Buggy
// ❌ Not captured by an Error Boundary
setTimeout(() => {
throw new Error("boom");
}, 0);Correct
// ✅ Capture at the source
setTimeout(() => {
try {
riskyWork();
} catch (e) {
setError((e as Error).message);
}
}, 0);Fix
Wrap risky subtrees with a local Error Boundary and provide an explicit recovery action. The crash no longer wipes the entire surface — it is replaced by a controlled fallback.
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
reset = () => this.setState({ hasError: false, error: null });
render() {
if (this.state.hasError) {
return <Fallback onReset={this.reset} />;
}
return this.props.children;
}
}Key takeaways
- A render-time exception can take down an entire subtree.
- Error Boundaries isolate failures and enable recovery.
- They do NOT capture async errors.
- Always define a recovery strategy.
Approach & tradeoffs
Approach
- Identify risky subtrees (3rd-party UI, fragile data, complex conditional rendering).
- Wrap them with local Error Boundaries.
- Provide recovery actions (reset/retry).
- Log failures for observability.
- Catch async errors at the source.
Tradeoffs
- Additional plumbing: fallbacks, resets, and state.
- Over-resetting can destroy user context.
- Under-resetting risks inconsistent UI.
- Boundary size matters — too large hides bugs, too small fragments UX.