Back to blog
Article

Error boundaries that phone home instead of showing a blank screen

Error boundaries that phone home instead of showing a blank screen
S

StriveBit

4 min readWeb Applications

Error boundaries that phone home instead of showing a blank screen

A client reported that users on a specific checkout flow were seeing a blank white screen. No console errors in our staging environment, no Sentry alerts, nothing in the server logs. The users were on mid-range Android devices over spotty connections, and the crash happened after a route transition that worked fine on every device we tested locally.

React error boundaries catch uncaught render errors, but the default behavior — swallowing the error and showing nothing — is worse than a crash. A native crash at least gives the OS a signal. A React boundary that renders `null` leaves the user staring at whitespace with no way to report it.

The first fix is giving the boundary an actual fallback UI. Not a generic "something went wrong" — a recovery path that lets the user retry or navigate elsewhere:

import { Component, type ReactNode } from "react";

type Props = { children: ReactNode; resetKey: string };
type State = { error: Error | null };

class ErrorBoundary extends Component<Props, State> {
  state: State = { error: null };

  static getDerivedStateFromError(error: Error) {
    return { error };
  }

  componentDidCatch(error: Error, info: { componentStack: string }) {
    logError({
      message: error.message,
      stack: error.stack,
      componentStack: info.componentStack,
      url: window.location.href,
      resetKey: this.props.resetKey,
      timestamp: Date.now(),
    });
  }

  componentDidUpdate(prev: Props) {
    if (prev.resetKey !== this.props.resetKey) {
      this.setState({ error: null });
    }
  }

  render() {
    if (this.state.error) {
      return (
        <div role="alert">
          <p>This screen failed to load.</p>
          <button onClick={() => this.setState({ error: null })}>
            Try again
          </button>
        </div>
      );
    }
    return this.props.children;
  }
}

The `resetKey` prop matters more than it looks. Without it, the boundary stays in its error state permanently — the user clicks "try again," nothing changes, and you have a stuck view. By tying reset to a route parameter or a key that changes on navigation, the boundary recovers when the user moves to a different screen.

The `logError` call is where most implementations go wrong. We have seen teams wire this to `console.error` and call it a day, which works in development and nowhere else. The call needs to go to whatever logging service you actually check. For most of our Next.js client work, that is Sentry, and the integration is one call:

import * as Sentry from "@sentry/nextjs";

function logError(data: {
  message: string;
  stack?: string;
  componentStack: string;
  url: string;
  resetKey: string;
  timestamp: number;
}) {
  Sentry.captureMessage(data.message, {
    level: "error",
    contexts: {
      react: { componentStack: data.componentStack },
      route: { url: data.url, resetKey: data.resetKey },
    },
    extra: { timestamp: data.timestamp, stack: data.stack },
  });
}

The `componentStack` from `componentDidCatch` is the part that makes the error actionable. Without it, you get a message like "Cannot read properties of undefined (reading 'price')" and no idea which component threw. With it, Sentry shows the full render path that led to the crash, which is usually enough to locate the problem without a reproduction.

Where you place the boundary determines what you lose when it fires. Wrapping the entire app in one boundary means any render error takes down the whole page. That made sense for the blank-screen problem we started with, but it is usually worth being more granular. We put boundaries around the major route segments and around components that depend on external data — a product list, a payment form, a search results panel. A crash in the product list should not take out the navigation header.

Next.js has its own error boundary convention: `error.tsx` files in the app router catch errors for their segment automatically. The built-in mechanism handles the lifecycle, but the logging still requires the same manual wiring. The file exports a component that receives the error as a prop, and you call your logging service in a `useEffect` rather than `componentDidCatch`. Same data, different hook.

The blank-screen bug from our client turned out to be a `useMemo` that crashed on a null value when a network request returned an empty response body. The boundary caught it, the fallback let users retry the request, and the Sentry alert included the component stack pointing at the exact memo. That is the whole point — the boundary should not just hide the crash, it should make the crash findable.

Back to all articles

Ready to build something great?

We help ambitious teams build software that lasts. If you're interested in working with us or want to discuss your project, let's connect.

Get in touch