Skip to content

Error Boundaries Interview Questions & Answers

20 questions Updated 2026-06-24 Share:

React error boundary interview questions — componentDidCatch, getDerivedStateFromError, fallback UI, error recovery, async errors, and react-error-boundary library.

Read the in-depth guideReact Error Boundaries — Complete Interview Guide(opens in new tab)
20 of 20

An error boundary is a React class component that catches JavaScript errors anywhere in its child component tree, logs them, and renders a fallback UI instead of crashing the whole application.

Before error boundaries existed, a single uncaught render error would unmount the entire React tree and leave users with a blank screen. Error boundaries provide fault isolation — only the subtree that threw is replaced with a fallback; everything outside the boundary keeps running normally.

// Without an error boundary, one bad render kills the whole app.
// With one, only the ErrorWidget section crashes — the rest survives.
<App>
  <Header />           {/* still renders fine */}
  <ErrorBoundary fallback={<p>Widget failed.</p>}>
    <ErrorWidget />    {/* throws → caught here, fallback shown */}
  </ErrorBoundary>
  <Footer />           {/* still renders fine */}
</App>

Rule of thumb: An error boundary is a circuit breaker — it stops one bad component from taking down the entire user interface.

getDerivedStateFromError is a static lifecycle method called during the render phase when a descendant throws. It receives the thrown error and must return an object that is merged into the component's state — typically a flag that switches the component from rendering children to rendering the fallback UI.

Because it runs during the render phase it must be pure and side-effect free. Use it only to update state; do all logging in componentDidCatch instead.

class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };

  // Called during render phase — update state to trigger fallback
  static getDerivedStateFromError(error) {
    return { hasError: true, error }; // pure — no side effects here
  }

  render() {
    if (this.state.hasError) {
      return <p>Something went wrong.</p>; // fallback UI
    }
    return this.props.children; // normal path
  }
}

Rule of thumb: getDerivedStateFromError answers what to show; it must stay pure — keep it to one return statement that updates state.

componentDidCatch is called during the commit phase, after the fallback UI has been painted. Unlike getDerivedStateFromError it receives both the error and an info object ({ componentStack }) that contains the component stack trace. It is the right place for side effects such as logging to an error monitoring service.

The two methods are complementary: getDerivedStateFromError flips the state to show the fallback; componentDidCatch does the logging.

class ErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true }; // render phase — only flip the flag
  }

  componentDidCatch(error, info) {
    // Commit phase — side effects are safe here
    // info.componentStack is the React component call stack
    logErrorToSentry(error, { extra: { componentStack: info.componentStack } });
  }

  render() {
    return this.state.hasError
      ? <FallbackUI />
      : this.props.children;
  }
}

Rule of thumb: getDerivedStateFromError = pure state update (render phase); componentDidCatch = side effects like logging (commit phase). Use both together.

Error boundaries require getDerivedStateFromError and componentDidCatch, which are class lifecycle methods with no hook equivalents in React's public API. The React team has acknowledged this gap but has not shipped a functional replacement — as of React 18 you still need a class component.

The react-error-boundary library wraps a class boundary behind a friendly functional API, which is the recommended way to avoid writing class components yourself in modern codebases.

// The class is unavoidable at the boundary itself…
class ErrorBoundary extends React.Component {
  state = { hasError: false };
  static getDerivedStateFromError() { return { hasError: true }; }
  render() {
    return this.state.hasError
      ? this.props.fallback
      : this.props.children;
  }
}

// …but you can wrap it so callers use a clean functional component:
function SafeWidget({ children }) {
  return (
    <ErrorBoundary fallback={<p>Widget failed</p>}>
      {children}
    </ErrorBoundary>
  );
}

Rule of thumb: You need a class somewhere in the chain — use react-error-boundary to hide it so the rest of your codebase stays functional.

Error boundaries catch errors that occur during rendering, lifecycle methods, and constructors of class components in the tree below them. They do NOT catch:

  • Event handlers — React does not call event handlers during rendering; wrap them in a regular try/catch instead.
  • Asynchronous code — errors in setTimeout, Promises, or async/await are not caught because they happen outside React's call stack.
  • Server-side rendering — boundaries only work client-side.
  • Errors thrown inside the boundary itself — a boundary cannot catch its own render errors.
function BadButton() {
  const handleClick = () => {
    try {
      riskyOperation(); // must use try/catch — boundary won't catch this
    } catch (err) {
      reportError(err);
    }
  };
  return <button onClick={handleClick}>Go</button>;
}

async function fetchData() {
  // Promise rejection — NOT caught by error boundary
  // Use .catch() or try/catch inside useEffect instead
  const data = await api.get('/things');
}

Rule of thumb: If the error happens outside a React render call — in a callback or a Promise — an error boundary will not catch it.

Coarser boundaries protect large sections but may hide too much on failure. Finer boundaries give surgical fallbacks but add boilerplate. A typical three-level strategy works well:

  1. App/root level — one top-level boundary as a last resort fallback (blank screen prevention).
  2. Route/page level — a boundary per route so a broken page doesn't kill navigation.
  3. Widget/feature level — boundaries around independent sidebar panels, recommendation carousels, or ad slots that can fail without affecting the main content.
<RootErrorBoundary>        {/* level 1 — last resort */}
  <Router>
    <Routes>
      <Route                 /* level 2 — per route */
        element={
          <RouteErrorBoundary>
            <DashboardPage />
          </RouteErrorBoundary>
        }
      />
    </Routes>
    <Sidebar>
      <WidgetErrorBoundary> {/* level 3 — isolated widget */}
        <RecommendationsPanel />
      </WidgetErrorBoundary>
    </Sidebar>
  </Router>
</RootErrorBoundary>

Rule of thumb: Place a boundary wherever you can write a meaningful, scoped fallback message — the finer the boundary, the better the user experience.

A well-designed fallback UI tells users what happened, what they can do next, and ideally provides a recovery action (reload, retry, go home). Avoid exposing raw error messages or stack traces in production.

Good fallback UIs are:

  • Scoped — show only in the crashed section, not full-page unless necessary.
  • Actionable — include a retry button or a link to the home page.
  • Branded — styled consistently, not a plain white box.
  • Non-alarming — phrase errors in friendly, non-technical language.
function FallbackUI({ error, resetErrorBoundary }) {
  return (
    <div role="alert" className="error-panel">
      <h2>This section couldn't load.</h2>
      {/* Never show error.message to users in production */}
      <p>Try refreshing or come back later.</p>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  );
}

Rule of thumb: A fallback UI should answer "what broke and what can I do?" — never dump a raw error message onto users.

Recovery works by resetting the boundary's error state so it re-renders its children on the next render. In a hand-rolled boundary, expose a reset method or pass a onReset callback. In react-error-boundary, use the resetErrorBoundary prop injected into the fallback component.

The key subtlety: if the component that threw hasn't changed, it will throw again immediately. Pair the reset with a key change or a data refetch so the re-render has a chance to succeed.

class ErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError() { return { hasError: true }; }

  // Expose reset so parent or fallback can call it
  reset = () => this.setState({ hasError: false });

  render() {
    if (this.state.hasError) {
      return (
        <FallbackUI onRetry={this.reset} />  // pass reset down
      );
    }
    return this.props.children;
  }
}

Rule of thumb: Always give users a retry path — but also make sure the retry actually fixes the underlying condition, not just re-throws the same error.

react-error-boundary is the community-standard wrapper that adds four key features on top of the raw class API:

  1. <ErrorBoundary> — a ready-made class boundary that accepts fallback, FallbackComponent, onError, onReset, and resetKeys props.
  2. FallbackComponent — receives { error, resetErrorBoundary } so the fallback can trigger recovery without prop drilling.
  3. resetKeys — an array of values; when any changes the boundary auto-resets, enabling data-driven recovery.
  4. useErrorBoundary() — hook that lets any functional component throw errors up to the nearest boundary programmatically (useful for async errors).
import { ErrorBoundary } from 'react-error-boundary';

function Fallback({ error, resetErrorBoundary }) {
  return (
    <div role="alert">
      <p>Error: {error.message}</p>
      <button onClick={resetErrorBoundary}>Retry</button>
    </div>
  );
}

// resetKeys: boundary auto-resets whenever `userId` changes
<ErrorBoundary FallbackComponent={Fallback} resetKeys={[userId]}>
  <UserProfile userId={userId} />
</ErrorBoundary>

Rule of thumb: Use react-error-boundary in production — it removes the need to write class components and handles the reset logic correctly.

useErrorBoundary (from react-error-boundary) returns { showBoundary, resetBoundary }. Calling showBoundary(error) throws the error up to the nearest <ErrorBoundary> as if it had been thrown during rendering. This is the standard way to route async and event-handler errors through a boundary, since those errors are not caught automatically.

import { useErrorBoundary } from 'react-error-boundary';

function UserList() {
  const { showBoundary } = useErrorBoundary();

  useEffect(() => {
    fetchUsers()
      .then(setUsers)
      .catch((err) => {
        showBoundary(err); // manually escalate to nearest boundary
      });
  }, []);

  return <ul>{/* ... */}</ul>;
}

Rule of thumb: Use showBoundary from useErrorBoundary any time you need to route a non-render error (async, event handler) into the boundary system.

resetKeys is an array of values passed to <ErrorBoundary>. Whenever any value in the array changes between renders, the boundary automatically resets its error state and re-renders its children. This enables data-driven recovery without requiring the user to click a retry button.

Common use cases: resetting when the user navigates to a different resource (changing userId, postId), or when a query key changes after a refetch.

// Automatically retry when the user switches to a different profile
<ErrorBoundary
  FallbackComponent={ProfileError}
  resetKeys={[userId]}       // boundary resets whenever userId changes
  onReset={() => refetch()}  // also trigger a fresh network request
>
  <UserProfile userId={userId} />
</ErrorBoundary>

Rule of thumb: Use resetKeys when navigating between resources — it gives every new resource a clean slate without manual reset button clicks.

componentDidCatch is the correct hook for external logging because it runs during the commit phase where side effects are safe. It receives both the error and info.componentStack, which together give a useful trace for debugging. Pass both to your monitoring SDK.

With react-error-boundary, use the onError prop instead of subclassing.

import * as Sentry from '@sentry/react';

// Option 1: hand-rolled class boundary
componentDidCatch(error, info) {
  Sentry.captureException(error, {
    extra: { componentStack: info.componentStack },
  });
}

// Option 2: react-error-boundary — onError prop (preferred)
<ErrorBoundary
  FallbackComponent={Fallback}
  onError={(error, info) => {
    Sentry.captureException(error, {
      extra: { componentStack: info.componentStack },
    });
  }}
>
  <App />
</ErrorBoundary>

Rule of thumb: Always log both the error and the component stack — error.message tells you what crashed; componentStack tells you where.

<Suspense> handles loading states (a component "suspends" by throwing a Promise); error boundaries handle error states (a component throws an Error). The two are designed to compose: wrap a <Suspense> inside an <ErrorBoundary> to cover both loading and failure for the same subtree.

In concurrent mode, React can retry suspended trees. If a retry results in an Error (not a Promise), the nearest error boundary catches it.

// Layered: error boundary wraps Suspense — covers both failure modes
<ErrorBoundary FallbackComponent={ErrorFallback}>
  <Suspense fallback={<Spinner />}>
    {/* LazyComponent suspends while loading → shows Spinner     */}
    {/* If it throws an Error instead → ErrorBoundary catches it */}
    <LazyComponent />
  </Suspense>
</ErrorBoundary>

Rule of thumb: Wrap every <Suspense> in an <ErrorBoundary> — Suspense only handles the "pending" state; errors still need a boundary.

Concurrent React introduces partial rendering and retries, which changes some boundary behavior:

  • React may attempt to render a subtree multiple times before committing, so getDerivedStateFromError might be called more than once for the same error. The method must remain pure.
  • With startTransition, React can defer a failing render and keep the previous UI visible rather than immediately showing the boundary fallback, giving a smoother degradation.
  • In Strict Mode (development only) React double-invokes render to surface impure code — componentDidCatch is not double-invoked, but getDerivedStateFromError may be.
// Wrap a non-urgent update in a transition:
// if it errors, React keeps showing the stale UI instead of the fallback
const [isPending, startTransition] = useTransition();

startTransition(() => {
  setResourceId(newId); // error here → boundary catches, but only after transition
});

Rule of thumb: Keep getDerivedStateFromError pure — concurrent mode may call it multiple times before a single commit.

A boundary cannot catch its own errors. If render() or any lifecycle in the boundary class itself throws, React walks up the tree and looks for the next ancestor error boundary. If none exists, the entire React tree is unmounted.

This means every error boundary implicitly relies on its parent being error-free — or being wrapped in another boundary. In practice, nest a minimal top-level boundary (that itself contains no complex logic) as the outermost root to catch anything a deeper boundary misses.

// A boundary that itself might fail — bad practice:
class RiskyBoundary extends React.Component {
  static getDerivedStateFromError() { return { hasError: true }; }
  render() {
    if (this.state.hasError) {
      return <ComplexFallbackThatMightAlsoThrow />; // dangerous!
    }
    return this.props.children;
  }
}

// Keep fallback UI extremely simple — plain HTML, no data fetching:
render() {
  if (this.state.hasError) {
    return <p>Something went wrong.</p>; // safe, cannot throw
  }
  return this.props.children;
}

Rule of thumb: Keep fallback UI dead simple — it should be plain HTML or a static string, never something that fetches data or renders lazily.

The standard approach is to render a component that intentionally throws and assert that the fallback UI appears. React Testing Library does not suppress the console error from React's internal boundary reporting, so you should spy on console.error and suppress it to keep test output clean.

import { render, screen } from '@testing-library/react';
import { ErrorBoundary } from 'react-error-boundary';

// A component that always throws
function Bomb() {
  throw new Error('💥 test error');
}

test('renders fallback when child throws', () => {
  // Suppress React's console.error noise in test output
  const spy = jest.spyOn(console, 'error').mockImplementation(() => {});

  render(
    <ErrorBoundary fallback={<p>Boundary caught it</p>}>
      <Bomb />
    </ErrorBoundary>
  );

  expect(screen.getByText('Boundary caught it')).toBeInTheDocument();
  spy.mockRestore(); // always restore after test
});

Rule of thumb: Suppress console.error during boundary tests to keep output clean, but always restore the spy — suppressing it globally hides real issues.

Test recovery by simulating the retry action (clicking the button) and asserting that the children render successfully after the error condition is cleared. Use a stateful wrapper to toggle whether the child throws, simulating a data fix.

let shouldThrow = true;

function MaybeThrow() {
  if (shouldThrow) throw new Error('temporary failure');
  return <p>Content loaded</p>;
}

test('retries successfully after reset', async () => {
  const spy = jest.spyOn(console, 'error').mockImplementation(() => {});

  render(
    <ErrorBoundary FallbackComponent={({ resetErrorBoundary }) => (
      <button onClick={() => {
        shouldThrow = false;     // fix the condition before reset
        resetErrorBoundary();    // trigger boundary reset
      }}>Retry</button>
    )}>
      <MaybeThrow />
    </ErrorBoundary>
  );

  await userEvent.click(screen.getByText('Retry'));
  expect(screen.getByText('Content loaded')).toBeInTheDocument();
  spy.mockRestore();
});

Rule of thumb: Fix the error condition before calling resetErrorBoundary — otherwise the boundary resets and immediately throws again.

Error boundaries protect against uncaught errors in the render cycle — they intercept errors in render, lifecycle methods, and constructors. They provide a declarative, component-scoped safety net.

try/catch is for imperative code — event handlers, async operations, and utility functions outside the React render cycle where boundaries cannot reach.

Scenario Tool
Component throws during render Error boundary
async fetch in useEffect try/catch + state
Button click handler throws try/catch
Lazy-loaded component fails Error boundary + Suspense
// Render error → boundary handles it automatically
function BadRender() {
  const data = JSON.parse(null); // throws during render → boundary catches
  return <div>{data.name}</div>;
}

// Async error → try/catch needed
async function loadUser(id) {
  try {
    return await api.getUser(id);
  } catch (err) {
    showBoundary(err); // escalate manually via useErrorBoundary
  }
}

Rule of thumb: Use boundaries for render errors, try/catch for everything else — then bridge async failures into boundaries via showBoundary.

Yes — React will propagate an unhandled error up the tree until it finds the nearest ancestor error boundary. Multiple nested boundaries each act as independent catch points for their own subtrees.

A healthy app has multiple boundaries at different levels:

  • The root boundary is the final backstop.
  • Route-level boundaries isolate page failures.
  • Widget-level boundaries isolate autonomous sections.
// Each boundary is independent — inner boundary failing falls up to outer
<AppBoundary>          {/* outer: catches everything the inner misses */}
  <Layout>
    <NavBoundary>      {/* isolates nav errors */}
      <Navigation />
    </NavBoundary>

    <PageBoundary>     {/* isolates current page errors */}
      <MainContent>
        <WidgetBoundary> {/* isolates a single widget */}
          <LiveFeed />
        </WidgetBoundary>
      </MainContent>
    </PageBoundary>
  </Layout>
</AppBoundary>

Rule of thumb: Think of boundaries like try/catch nesting — the innermost matching boundary catches the error; outer ones only fire if the inner one itself fails or doesn't exist.

In development (react-dom/development), React re-throws errors after calling the boundary so that the browser's DevTools overlay and the console both show the full stack trace. This means you'll see the error in the console even when a boundary catches it — that's intentional, not a bug.

In production (react-dom/production), the error is caught silently by the boundary, the fallback UI is shown, and no overlay appears. Users see only the fallback; developers see nothing unless logging (Sentry, etc.) is set up.

// Dev: React error overlay appears + console.error fires even with a boundary.
// This tells developers the error happened; the boundary still works in prod.

// A common mistake: developers test boundaries in dev, see the overlay,
// and think the boundary isn't working — it is; the overlay is just dev behavior.

componentDidCatch(error, info) {
  // This is your ONLY visibility into boundary-caught errors in production
  logToMonitoringService(error, info);
}

Rule of thumb: The dev error overlay does not mean the boundary failed — React shows it deliberately; always test production behavior with npm run build.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel