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.
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?