← Back to question bank
DebuggingSeniorHard#4024 · 35m

Debug error boundaries under interview pressure

A React screen using error boundaries passes simple tests but breaks during repeated interaction. Find the likely root cause, patch it, and describe the longer-term design improvement.

Answer Strategy

The broken pattern in this question is "one ErrorBoundary at the app root, no reset path". The first failure mode: any thrown render error in any subtree unmounts the entire UI, so a broken sidebar widget takes the whole product down. The second failure mode: the boundary never resets, so once the user sees the fallback they are stuck until refresh. Even a route change does not clear the error. The fix is per-surface boundaries with a resetKeys prop that clears the error when the keys change.

Locate the boundary by asking "which subtree can fail independently?". A page header, a chart, a comments panel, a third-party widget — each gets its own ErrorBoundary with a localized fallback. Pair with a resetKeys prop that lists the values whose change should clear stale errors: route key, user id, query parameters. The boundary becomes a self-healing surface instead of a dead end.

Adjacent traps: catching errors that should propagate (network errors belong in data layer, not UI boundaries), forgetting to log to monitoring (the user sees a friendly message, but ops never knows), and using react-error-boundary without resetKeys (the same stuck-state bug returns). Add componentDidCatch for monitoring, add resetKeys for navigation recovery, and scope the boundary to a real surface, not the whole app.

Regression Fix: Per-Surface ErrorBoundary With resetKeys

The fixed PageShell uses an ErrorBoundary scoped to the surface, with resetKeys tied to the routeKey so navigation clears stale errors.

// THE BUG: a single ErrorBoundary at the app root caught any thrown
// render error, so one broken widget unmounted the entire UI. Worse,
// the boundary did not reset when the user navigated away — the user
// got stuck on the fallback until a hard refresh. The fix is nested
// boundaries scoped to each independent surface, with a resetKeys
// prop so route changes clear stale errors automatically.

type State = { error: Error | null };
type Props = {
  fallback: (params: { error: Error; reset: () => void }) => React.ReactElement;
  resetKeys?: ReadonlyArray<unknown>;
  children: React.ReactNode;
};

export class ErrorBoundary extends React.Component<Props, State> {
  state: State = { error: null };

  static getDerivedStateFromError(error: Error): State {
    return { error };
  }

  componentDidUpdate(prevProps: Props) {
    if (this.state.error && this.didKeysChange(prevProps.resetKeys, this.props.resetKeys)) {
      this.setState({ error: null });
    }
  }

  didKeysChange(prev?: ReadonlyArray<unknown>, next?: ReadonlyArray<unknown>) {
    if (!prev || !next || prev.length !== next.length) return true;
    return prev.some((value, index) => !Object.is(value, next[index]));
  }

  reset = () => this.setState({ error: null });

  render() {
    if (this.state.error) return this.props.fallback({ error: this.state.error, reset: this.reset });
    return this.props.children;
  }
}

// Compose at the surface level. The chart fails without taking the page
// down; the user can keep navigating and other widgets stay alive.
export function PageShell({
  routeKey,
  children,
}: {
  routeKey: string;
  children: React.ReactNode;
}) {
  return (
    <ErrorBoundary
      resetKeys={[routeKey]}
      fallback={({ error, reset })=> (
        <div role="alert">
          <p>Something went wrong: {error.message}</p>
          <button onClick={reset}>Retry</button>
        </div>
      )}
    >
      {children}
    </ErrorBoundary>
  );
}

Testing Strategy

Convert the answer into observable behavior. In a mid-senior interview, say which behaviors are covered by unit tests, interaction tests, accessibility checks, and one browser smoke path.

Reproduce
Render with a single root boundary and a child that throws on a sub-route. Assert the entire page collapses to fallback. The fix scopes the boundary to the surface and the rest of the page stays alive.
Patch
Wrap independent surfaces in their own boundaries. Add resetKeys that include the route key. Test that route changes clear stale errors so the user is not stuck.
Prevent
Wire componentDidCatch to your monitoring SDK so the boundary is observable. Pair with a smoke test that renders a deliberately throwing child and asserts the rest of the page is interactive.
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';

function ThrowingChild({ shouldThrow }: { shouldThrow: boolean }) {
  if (shouldThrow) throw new Error('boom');
  return <span>safe</span>;
}

describe('ErrorBoundary regression', () => {
  it('catches a render error and shows a recoverable fallback', () => {
    const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
    render(
      <PageShell routeKey="/a">
        <ThrowingChild shouldThrow={true} />
      </PageShell>
    );
    expect(screen.getByRole('alert')).toHaveTextContent('boom');
    fireEvent.click(screen.getByText('Retry'));
    consoleError.mockRestore();
  });

  it('clears the fallback when resetKeys change (route change)', () => {
    const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
    function App({ routeKey, broken }: { routeKey: string; broken: boolean }) {
      return (
        <PageShell routeKey={routeKey}>
          <ThrowingChild shouldThrow={broken} />
        </PageShell>
      );
    }
    const { rerender } = render(<App routeKey="/a" broken={true} />);
    expect(screen.getByRole('alert')).toBeInTheDocument();
    rerender(<App routeKey="/b" broken={false} />);
    // Route changed AND child is no longer throwing — the boundary
    // resets and the safe child renders.
    expect(screen.getByText('safe')).toBeInTheDocument();
    consoleError.mockRestore();
  });
});

Interviewer Signal

Tests whether you debug from ownership and lifecycle instead of random dependency-array edits.

Constraints

  • State a hypothesis before changing code.
  • Name what evidence would confirm the bug.
  • Avoid broad rewrites unless the current API cannot express the behavior.

Model Answer Shape

  • Reproduce the failing sequence first.
  • Inspect ownership boundaries: local state, props, effects, subscriptions, and server data.
  • Patch the minimal broken boundary and add a regression test.

Tradeoffs

  • A minimal patch reduces risk, but repeated lifecycle bugs often justify a small reducer or custom hook.
  • Adding dependencies can silence lint warnings while still preserving the wrong ownership model.

Edge Cases

  • Double clicks and repeated submissions.
  • Slow network responses arriving out of order.
  • Component remount with stale persisted state.

Testing And Proof

  • Failing interaction sequence.
  • Out-of-order async response.
  • Unmount cleanup.

Follow-Ups

  • What would the code review comment say?
  • What metric or log would show this in production?