ProgramGuru

Error Boundaries and Suspense

Error Boundaries and Suspense

A report widget may fail to render while the rest of the school dashboard is still useful. Another widget may simply be waiting for its code to arrive. Calling both situations “loading” hides whether students should wait or seek recovery.

An Error Boundary supplies fallback UI for supported rendering errors in its descendant tree. Suspense supplies fallback UI while supported descendants suspend. Failure and pending work are different states with different recovery needs.

  1. Separate outcomes

    Distinguish failure from supported pending work.

  2. Place a boundary

    Protect a useful part of the interface.

  3. Handle other errors

    Manage event and ordinary request failures explicitly.

Follow the user’s interaction

  1. Region

    The report has its own boundary.

  2. Pending

    Supported suspension can show a loading fallback.

  3. Failed

    A render error can show a failure fallback.

Read this relationship alongside the complete code. This diagram explains the flow; it does not execute React.

A boundary describes a region of responsibility

A report failure should not necessarily replace the entire dashboard. Place a boundary around a coherent region and choose a fallback that names what is unavailable. React’s built-in Error Boundary pattern uses a class with getDerivedStateFromError; regular function components can still be its children.

Suspense responds to supported suspension, such as lazy component loading or a framework’s compatible data source. It does not observe every asynchronous operation. React explains Error Boundaries, while the Suspense reference describes the separate pending-work mechanism.

A healthy report inside both boundaries

Initially Report renders Maya: 75 normally. The Suspense fallback is not displayed because this report does not suspend. Selecting Try damaged report makes Report throw while rendering; the surrounding Error Boundary replaces that region with Report cannot be displayed. The heading and button remain outside it.

This is a deliberate render-failure demonstration, so development tools may report the thrown error even though the boundary displays fallback UI. The boundary does not fix the record or retry it automatically. Its failed state remains set until a deliberate reset or remount.

React · JavaScript with JSX

import React, { Component, Suspense, useState } from 'react';
class ReportBoundary extends Component {
  constructor(props) { super(props); this.state = { failed: false }; }
  static getDerivedStateFromError() { return { failed: true }; }
  render() {
    return this.state.failed ? <p role="alert">Report cannot be displayed</p> : this.props.children;
  }
}
function Report({ broken }) {
  if (broken) throw new Error('Invalid report record');
  return <p>Maya: 75</p>;
}
export default function App() {
  const [broken, setBroken] = useState(false);
  return <main><h1>School reports</h1>
    <button onClick={() => setBroken(true)}>Try damaged report</button>
    <ReportBoundary><Suspense fallback={<p>Loading report</p>}>
      <Report broken={broken} />
    </Suspense></ReportBoundary>
  </main>;
}

Expected screen and behavior

Initially: School reports, Try damaged report and Maya: 75.
A damaged render displays Report cannot be displayed; development error reporting is expected.

The failure happens during rendering

React · the boundary and throwing child

import React, { Component, Suspense, useState } from 'react';
class ReportBoundary extends Component {
  constructor(props) { super(props); this.state = { failed: false }; }
  static getDerivedStateFromError() { return { failed: true }; }
  render() {
    return this.state.failed ? <p role="alert">Report cannot be displayed</p> : this.props.children;
  }
}
function Report({ broken }) {
  if (broken) throw new Error('Invalid report record');
  return <p>Maya: 75</p>;
}
export default function App() {
  const [broken, setBroken] = useState(false);
  return <main><h1>School reports</h1>
    <button onClick={() => setBroken(true)}>Try damaged report</button>
    <ReportBoundary><Suspense fallback={<p>Loading report</p>}>
      <Report broken={broken} />
    </Suspense></ReportBoundary>
  </main>;
}

The button handler only updates broken. During the subsequent render, Report throws. That location matters: a surrounding Error Boundary can handle this supported descendant render error. A try/catch around setBroken would not surround the later React rendering work.

A boundary cannot catch its own rendering failure. Keep fallback code simple so it does not depend on the damaged record. Production monitoring can use the appropriate reporting integration, but avoid sending private report contents merely because a diagnostic message is convenient.

Real local lazy loading, with an artificial wait

This complete alternative App.jsx uses React.lazy and Suspense. The loader returns a promise for a module-shaped object with a default component. A local 300-millisecond timer deliberately delays that promise; it performs no network request and does not measure deployment performance. In a mounted browser it first shows Loading report, then Maya: 75.

In a real code-splitting setup, lazy commonly receives a dynamic import produced by the bundler. The fallback belongs to the boundary, while the loader belongs outside App so each render does not create a new component type. Server rendering needs the server’s compatible streaming behavior; a browser demonstration does not establish those server capabilities.

React · JavaScript with JSX

import React, { lazy, Suspense } from 'react';
const Report = lazy(() => new Promise(resolve => {
  setTimeout(() => resolve({
    default: function Report() { return <p>Maya: 75</p>; }
  }), 300);
}));
export default function App() {
  return <main><h1>School reports</h1>
    <Suspense fallback={<p>Loading report</p>}><Report /></Suspense>
  </main>;
}

Expected screen and behavior

Initially: School reports · Loading report
After the local delay: School reports · Maya: 75

Worked practice: change the failure explanation

Give the failure fallback a message that names the report and advises a safe next step. Keep it independent of the broken record. The complete answer below changes only the fallback content; initially the healthy report still appears.

After an intentional damaged render, the explanation is Report unavailable. Contact the school office. Do not add a Retry button that has no reset behavior: a useful action must actually clear the relevant failure and attempt an operation that can succeed.

React · JavaScript with JSX

import React, { Component, Suspense, useState } from 'react';
class ReportBoundary extends Component {
  constructor(props) { super(props); this.state = { failed: false }; }
  static getDerivedStateFromError() { return { failed: true }; }
  render() {
    return this.state.failed ? <p role="alert">Report unavailable. Contact the school office.</p> : this.props.children;
  }
}
function Report({ broken }) {
  if (broken) throw new Error('Invalid report record');
  return <p>Maya: 75</p>;
}
export default function App() {
  const [broken, setBroken] = useState(false);
  return <main><h1>School reports</h1>
    <button onClick={() => setBroken(true)}>Try damaged report</button>
    <ReportBoundary><Suspense fallback={<p>Loading report</p>}>
      <Report broken={broken} />
    </Suspense></ReportBoundary>
  </main>;
}

Expected screen and behavior

Initially: Maya: 75.
The damaged render displays Report unavailable. Contact the school office.

Not every failure belongs to these boundaries

Ordinary event-handler errors and asynchronous callback errors are not generally caught by an Error Boundary. Validate forms and handle request rejection in their owning event or data layer. A missing student can often be an expected UI state rather than an exception that replaces the whole region.

Fetching in an Effect does not by itself activate Suspense. Give that design explicit loading and error state, or use an actual compatible framework or data integration. Keep the existing useful screen visible when appropriate and avoid making every small pending operation blank the entire application.

Check the fallback

Why is Loading report absent in the initial example?
Choose one answer. You can try again.

Answer and explanation

Report renders normally and does not suspend.

Correct. A Suspense wrapper alone does not create pending work.

Apply error ownership

An ordinary save-button handler receives a rejected request. What should the handler or data layer do?
Choose one answer. You can try again.

Answer and explanation

Handle the rejection and present an appropriate save failure.

Correct. An ordinary request failure needs explicit ownership.

Show whether work is waiting or has failed

An Error Boundary isolates supported render failures; Suspense coordinates supported suspension. Place them around meaningful regions and provide truthful fallbacks. Handle expected missing data, event errors and ordinary asynchronous failures at their actual owner.

Connect these choices with explicit data-loading states and investigating the underlying failure.

Reaching the recap and completing the quizzes are separate milestones.

    Next lesson: Render Outside a DOM Container with Portals →