React

Retrying after a Suspense failure

Suspense has no retry button: you retry by remounting the boundary with a new key and invalidating the cache that threw the promise.

Key points

  • Suspense handles the pending promise, the error boundary handles the rejection: you need both.
  • Changing the boundary key unmounts the subtree and clears both the error state and the pending promise.
  • When the cache keeps the rejected promise, the remount reads the same rejection, so evict it first.

Suspense offers no retry: to try again you remount the boundary with a new key and invalidate the cache that threw the promise. Without both steps the boundary reads the same rejection and stays on the error.

Who catches what

Suspense intercepts a promise thrown during render and shows the fallback until it settles. An error, including a rejected promise read by use(), does not pass through Suspense: it goes to the nearest error boundary. That is why a full retry needs both boundaries.

The schema with a key

The attempt counter lives above the error boundary and becomes the key of the boundary itself: a new key is the same as building a new boundary.

function Panel({ id }) {
  const [attempt, setAttempt] = useState(0);

  return (
    
          Load failed{" "}
           setAttempt(attempt + 1)}>Try again
        
      }
    >
      Loading}>
        
      
    
  );
}

Why the key is enough for state

React treats a different key as a different component: it unmounts the subtree, discards the error boundary state and starts from the initial render. The pending promise goes with the subtree, so the new attempt begins from zero.

The cache stays

When the data layer keeps the rejected promise, the remount finds it again and rethrows the same error. Evict the entry before incrementing the attempt, or the button appears to do nothing.

async function retry() {
  await cache.delete(`details:${id}`);
  setAttempt((n) => n + 1);
}

Order matters: the deletion awaits, then the counter changes. Reverse them and the new boundary may read the old entry before it disappears.

The command

react-error-boundary offers resetKeys and onReset, so the counter becomes the reset key without a hand-written boundary.

npm install react-error-boundary
npm ls react-error-boundary

Live records in the catalogue

Three records that show a loading state before the data: leads-data-table fills a table from a remote source, project-data-table filters rows that arrive later, area-chart draws once the series is ready. They are records among the 111,985 components in the catalogue.

Source and licence

React ships under the MIT licence and react-error-boundary under the MIT licence. The React documentation covers the fallback on the Suspense page and recovery on the error boundary page: read them together, because retry lives in the overlap.

The limitation

A retry through remount loses the subtree local state, the scroll position and the focus. An automatic retry on a 500 multiplies requests, so cap the number of attempts and keep the manual button as the default.

Where to continue

The basics are in Suspense; the boundary that catches errors is in error boundary pattern; the comparison with Next.js streaming is in streaming and Suspense in Next.js.

Frequently asked questions

Why does Suspense have no built-in retry?

Because it does not know who threw the promise or how to invalidate it. Retry belongs to the data layer, which knows the cache key, and to the boundary, which can remount.

Do I need a new key or just a setState?

You need a new key on the boundary, or resetKeys when you use react-error-boundary. A setState inside the same boundary does not clear the error state.

Does retry work without an error boundary?

No: a rejected promise throws an error that climbs to the root with no boundary and unmounts the app. The boundary is what turns a blank page into a button.

Can I retry automatically?

Yes, with an effect that increments the attempt after a delay, but cap it: with no ceiling a 500 error produces a burst of requests and burns quota.

What happens to the state of the remounted component?

It is lost. A remount is a new component, so local state, scroll and focus return to their initial values: lift the state above the boundary when it must survive.