Error Boundary Retry and Reset Patterns for Resilient React UIs
AI generated
{ }
React · Error Handling · Resilience
Error Boundary Retry and Reset Patterns
for resilient React UIs that recover on their own

An error boundary that shows a permanent gray wall after a failure is only half the solution. With resetKeys, retry buttons, and a clean reset mechanism, the fallback becomes a real recovery path instead of a dead end.

15 min read react-error-boundary resetKeys Retry UX

1. Why an error boundary alone is not enough

A classic error boundary in React catches rendering errors in its child tree and shows a fallback UI instead of a blank white screen. That is the starting point, but in a production application it is not enough: once the error has occurred, the boundary stays stuck in the error state permanently, because componentDidCatch sets internal state only once and React does not automatically re-render the component afterwards. The user sees an error message and, without a full page reload, has no way of getting back into a working state.

Resilient UIs therefore need two additional capabilities: an explicit way to reset the error state, and a mechanism that resets automatically on certain changes, for example when the route or a key prop changes. Both requirements can be implemented with plain React, but they become much more convenient with the react-error-boundary library by Kent C. Dodds, because it already ships the resetKeys pattern and an onReset callback instead of every team reinventing them for each boundary.

2. The resetKeys pattern of react-error-boundary

The ErrorBoundary component from react-error-boundary accepts a prop called resetKeys: an array of values the boundary compares on every render. If even one of these values changes between two renders, the boundary automatically resets its internal error state and tries to render the children again, without the user having to do anything. This is especially useful when an error is tied to a specific record, for example a product ID on a detail page: once the user navigates to a different product, the old error is no longer relevant and the boundary should stop showing it.

In practice, you typically pass the values the rendered content depends on, such as the product ID from URL parameters or a query key. Important detail: resetKeys compares values shallowly, so primitive values like strings or numbers work reliably, while freshly created objects or arrays count as 'changed' on every render and can trigger unwanted resets. That is why resetKeys should consist of stable, primitive identifiers rather than objects that get re-instantiated on every render.


import { ErrorBoundary } from 'react-error-boundary';
import { useParams } from 'react-router-dom';

function ProductErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div role="alert" className="rounded-lg border border-red-200 bg-red-50 p-4">
      <p className="font-semibold text-red-800">Could not load product</p>
      <p className="text-sm text-red-700">{error.message}</p>
      <button
        onClick={resetErrorBoundary}
        className="mt-2 rounded bg-red-600 px-3 py-1.5 text-sm text-white"
      >
        Try again
      </button>
    </div>
  );
}

function ProductDetailPage() {
  const { productId } = useParams();

  return (
    <ErrorBoundary
      FallbackComponent={ProductErrorFallback}
      resetKeys={[productId]}
    >
      <ProductDetail productId={productId} />
    </ErrorBoundary>
  );
}

3. A retry button for manual resets

react-error-boundary passes a function called resetErrorBoundary down to the fallback component. If the user calls it via a button, the boundary resets its internal error state and tries to render the children again, exactly like with resetKeys, only triggered manually instead of automatically. This is the right choice for errors whose cause is not tied to a recognizable prop, such as a temporary network failure while loading a widget, where the user should decide when a new attempt makes sense.

A plain reset of the UI state does not automatically resolve the underlying cause, however: if the component threw an error on first render because of a stale cache or a failed request, a simple remount would reproduce the same error immediately. That is why the retry button is usually combined with an explicit refetch function and a retry counter that changes the fallback UI after a few unsuccessful attempts, for example from 'Try again' to 'Please try again later', so the user is not offered an infinite loop.


import { useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary';

function RetryFallback({ error, resetErrorBoundary }) {
  const [attempts, setAttempts] = useState(0);
  const maxAttempts = 3;

  const handleRetry = () => {
    setAttempts((a) => a + 1);
    resetErrorBoundary();
  };

  return (
    <div role="alert" className="rounded-lg border p-4">
      <p>{error.message}</p>
      {attempts < maxAttempts ? (
        <button onClick={handleRetry}>Try again</button>
      ) : (
        <p className="text-sm text-gray-500">
          Please reload the page in a few minutes.
        </p>
      )}
    </div>
  );
}

function Widget({ refetch }) {
  return (
    <ErrorBoundary
      FallbackComponent={RetryFallback}
      onReset={() => refetch()}
    >
      <DataDrivenWidget />
    </ErrorBoundary>
  );
}

4. A custom error boundary class without a library

Error boundaries in React can still only be implemented as class components today, because they rely on the lifecycle methods static getDerivedStateFromError and componentDidCatch, for which there is no hook equivalent. getDerivedStateFromError is called synchronously during render and returns the new state that activates the fallback UI, while componentDidCatch handles side effects like logging afterwards. Anyone who does not want to add an extra dependency can implement both methods themselves with very little code.

For resetting without a library, the key remount trick has become the standard approach: you keep a counter in the parent state and pass it as the key to the boundary or its children. When the key changes, React removes the entire instance and creates a new one, which guarantees that the boundary's internal error state disappears as well, without having to implement a dedicated resetError method on the class. The trick is simple but effective because it relies on React's built in reconciliation behavior instead of custom state logic.


import { Component } from 'react';

class SimpleErrorBoundary extends Component {
  state = { hasError: false, error: null };

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

  componentDidCatch(error, info) {
    console.error('Boundary caught:', error, info.componentStack);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback(this.state.error);
    }
    return this.props.children;
  }
}

function Parent() {
  const [resetCount, setResetCount] = useState(0);

  return (
    <SimpleErrorBoundary
      key={resetCount}
      fallback={(error) => (
        <button onClick={() => setResetCount((c) => c + 1)}>
          Reload ({error.message})
        </button>
      )}
    >
      <RiskyChild />
    </SimpleErrorBoundary>
  );
}

5. Limits: what error boundaries do not catch

Error boundaries only catch errors thrown during rendering, in lifecycle methods, or in constructors of components below the boundary. They do not catch errors in event handlers such as onClick, errors in asynchronous code such as setTimeout callbacks or promise chains, errors during server side rendering, or errors that occur inside the boundary itself. A try/catch inside a fetch callback that only writes to the console will therefore never trigger the fallback UI of the surrounding boundary, no matter how the boundary is configured.

To make such asynchronous errors visible through the same boundary anyway, developers use the 'throw during render' trick: you keep the error in state and explicitly throw it on the next render, so React treats it like a regular render error and the next error boundary catches it. react-error-boundary provides the useErrorHandler hook, or a similar pattern in newer versions, for exactly this bridge between imperative async code and declarative error handling.


import { useState, useCallback } from 'react';

function useThrowAsyncError() {
  const [, setError] = useState();

  return useCallback((error) => {
    setError(() => {
      throw error;
    });
  }, []);
}

function AsyncWidget() {
  const throwAsyncError = useThrowAsyncError();

  useEffect(() => {
    fetchData().catch((err) => throwAsyncError(err));
  }, [throwAsyncError]);

  return <DataView />;
}

6. Granularity: where to place boundaries

A single error boundary around the entire app is quick to set up but has a decisive drawback: an error in one unimportant widget, like a product recommendation, drags the whole page into an error state, even though the rest of the application would work fine. At the other end of the spectrum, a boundary around every single component offers maximum isolation but creates massive boilerplate and lets closely related UI areas fall into error states independently of each other, which confuses users.

In practice a two tier strategy works best: a coarse, application wide boundary at the root as a final safety net, combined with targeted boundaries around self contained feature widgets like a shopping cart, a comment section, or a recommendation carousel. Each of these inner boundaries gets its own resetKeys that match the data context of that widget, so an error in the recommendation widget never brings down the checkout area on the same page.

7. Fallback UI and UX considerations

The fallback UI should clearly communicate to the user whether this is a temporary problem a retry can fix, or a permanent failure where a retry button only creates frustration. Technical details like stack traces do not belong in the visible UI in production, they belong in a separate logging system, while the visible message stays understandable and actionable, for example 'This section could not be loaded' instead of a raw error message from the JavaScript engine.

In terms of layout, the fallback UI should ideally take up the same amount of space as the actual content, so surrounding elements do not jump when the error occurs or gets resolved. For accessibility, a role='alert' on the fallback container is important so screen readers announce the error message automatically, and for critical errors, focus can additionally be moved explicitly to the error message so keyboard users do not miss it.

8. Reporting errors to monitoring tools like Sentry

Besides the error object, componentDidCatch also provides an info object with componentStack, which shows exactly where in the component tree the error occurred, often more valuable for debugging than the raw JavaScript stack trace. This information should be forwarded to a monitoring system such as Sentry, ideally enriched with context like the current resetKeys, the retry count, and relevant IDs, so recurring errors can be traced back to specific records.

react-error-boundary offers the onError prop on the ErrorBoundary component for exactly this, handling reporting in one central place in the code instead of repeating it in every single boundary. It is important to deduplicate repeats: if a user triggers the same error multiple times by clicking retry repeatedly, monitoring should not create a separate, equal weight event every time, but rather track the retry count as an extra field on the original event.


import * as Sentry from '@sentry/react';
import { ErrorBoundary } from 'react-error-boundary';

function App() {
  return (
    <ErrorBoundary
      FallbackComponent={AppFallback}
      onError={(error, info) => {
        Sentry.captureException(error, {
          contexts: { react: { componentStack: info.componentStack } },
          tags: { boundary: 'app-root' },
        });
      }}
    >
      <Router />
    </ErrorBoundary>
  );
}

9. Practical checklist and comparison of approaches

For small, self contained applications without external dependencies, a hand written boundary with key remount can be enough, since it requires no extra package and the code stays fully under your own control. But once multiple boundaries with different resetKeys, retry counters, and centralized error reporting are needed, react-error-boundary pays off, because it already implements these patterns robustly and well tested instead of every team rebuilding them from scratch.

The table below summarizes which approach fits which use case. As a rule of thumb: automatic reset via resetKeys for data dependent errors, a manual retry button for network related errors, and a combination of both for widgets that depend on external data as well as unstable connections.

Approach Automatic reset Effort Use case
Custom class without reset No Low Prototypes, one off fallbacks
Custom class with key remount No, manual via key Medium Small apps without an extra package
react-error-boundary without resetKeys No, retry button only Low Network dependent widgets
react-error-boundary with resetKeys Yes, on prop change Low Detail pages with a changing ID
react-error-boundary with onReset and retry counter Yes, plus limited manual attempts Medium Critical widgets with monitoring

Mironsoft

React architecture, performance, and Magento frontend integration

React frontends that stay fast instead of slowing down with every feature?

We review existing React applications for unnecessary re-renders, bloated bundles, and fragile state management, then build a frontend that stays performant and connects cleanly to Magento or other backends.

Performance Audit

Systematically measuring and fixing re-renders, bundle size, and load times.

State Architecture

Cleanly separating context, client state, and server state instead of mixing everything.

Magento Integration

Building robust, type-safe GraphQL or REST integration with Magento.

10. Summary

Error Boundary Retry Patterns: The Essentials at a Glance

resetKeys

Array of values that automatically resets the boundary on change, ideal for data dependent errors like a changing ID.

Retry button

Call resetErrorBoundary manually, combined with a refetch and an attempt counter to avoid infinite loops.

Key remount

Reset a custom boundary without a library by changing the parent's key, relies on React's reconciliation.

Monitoring

Send componentStack from componentDidCatch to Sentry or similar tools, carry the retry count as context.

11. FAQ: Error Boundary Retry Patterns: The Essentials at a Glance

1What is the difference between resetKeys and a manual retry button?
resetKeys triggers the reset automatically as soon as one of the passed values changes, for example a product ID after navigation. A retry button, on the other hand, is explicitly clicked by the user and calls the resetErrorBoundary function provided by react-error-boundary. Both mechanisms can be used together on the same boundary.
2Can I write error boundaries as a function component with hooks?
No, React currently offers no hook equivalent for getDerivedStateFromError and componentDidCatch, so error boundaries must be implemented as a class component. Libraries like react-error-boundary wrap that class internally, so in your own code you still only work with the declarative component and hooks like useErrorHandler.
3Why do my resetKeys keep triggering unwanted resets?
Usually it is because a freshly created object or array is passed as a resetKey, getting a new reference on every render even when its contents stay the same. The boundary compares resetKeys shallowly by reference, so only stable, primitive values like IDs or strings should be used as resetKeys.
4Do error boundaries also catch errors in event handlers?
No, error boundaries only catch errors that occur during rendering or in lifecycle methods. Errors in onClick or other event handlers still need to be handled with try/catch inside the handler itself, or thrown into the next render via a state trick so the boundary can catch them.
5How do I prevent an infinite retry loop for permanent errors?
An attempt counter inside the fallback component that tracks the number of retry clicks is the simplest safeguard. After a fixed number of attempts, for example three, hide the retry button and show a message to try again later instead of letting the user run into an endless click loop.
6Should I use a single global error boundary or many small ones?
A combination is most robust: a coarse boundary at the root as a last safety net, plus additional targeted boundaries around independent feature widgets. This keeps the rest of the page functional if only a single widget throws an error, while the global boundary catches catastrophic cases.
7How do I report errors from an error boundary to Sentry?
react-error-boundary offers the onError prop, which is called with the error object and the componentStack whenever an error is caught. There you call Sentry.captureException and can attach extra context such as the current retry count or relevant IDs as tags or extra data.
8What is the key remount trick and when do I need it?
With the key remount trick you pass the error boundary or its children a key that changes whenever a reset is desired, for example an incrementing counter. React then removes the old instance entirely and creates a new one, which also clears the internal error state. The trick is useful when you want to avoid depending on react-error-boundary.
9Does the fallback UI need to be the same size as the original content?
Ideally yes, so surrounding layout elements do not jump when an error occurs or gets resolved by a reset. Especially for widgets inside a grid or a list, an appropriately sized fallback UI creates a more stable, less jarring user experience.
10Does an error boundary also catch errors during server side rendering?
No, error boundaries only work within React's client side rendering cycle. Errors during a server side rendering pass, for example with renderToPipeableStream, must be handled through the dedicated onError and onShellError callbacks of that server rendering API, not through a client side boundary.