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.

Repro steps
  1. Press "Toggle crash render".
  2. Observe how the widget crashes and the shell-level fallback replaces the subtree.
  3. Use "Reset shell" to recover the region.
Buggyno local boundary
crash
false
Widget healthy. Trigger a crash to observe failure propagation.
Rule of thumb
Any subtree that can fail must be isolated.

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.
Broken invariant
Invariant: failures must be contained within a defined boundary. When a component throws, React aborts reconciliation and switches to a fallback UI.

Detected issue

Without isolation, a render crash replaces the subtree with an app-level fallback. User continuity is destroyed.

Detector
Status: OK

Reference (correct mental model)

Correct mental model

Incorrect

Assume success and render risky subtrees without isolation.

Incorrect / Buggy

// ❌ No isolation
<WidgetThatMayThrow />

Correct

Isolate risky subtrees with an Error Boundary and provide a recovery path.

Correct

// ✅ Isolation + recovery
<ErrorBoundary fallback={<Fallback />}>
  <WidgetThatMayThrow />
</ErrorBoundary>
Break it: Error Boundaries do NOT capture async failures
Repro steps
  1. Press "Throw in setTimeout".
  2. The error surfaces globally — the boundary does not intercept it.
  3. Use "Handle async safely" to capture the error at the source.
AsyncError Boundaries do NOT capture async errors (timers, promises, event handlers)
okCount
0
No handled error. UI is healthy.
Key point
Error Boundaries only capture synchronous render failures. Async failures bypass the boundary entirely.

Async: incorrect vs correct

Incorrect / Buggy

Incorrect snippet

// ❌ Not captured by an Error Boundary
setTimeout(() => {
  throw new Error("boom");
}, 0);

Correct

Correct snippet

// ✅ 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.

FixedErrorBoundary + recovery
crash
false
Widget healthy. Trigger a crash to observe failure propagation.

Error Boundary with recovery

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.
Common pitfall
A single global boundary is not resilience — it is delayed failure.