Debugging 2026-06-08

The White Screen of Death in Production React

The call came in at 4:50 on a Friday, because of course it did. A customer on the phone with our support person, saying the app was “just white.” Not an error page. Not a spinner. White. Works fine on my machine, works fine on staging, works fine for every other customer we asked. If you’ve ever had to debug a white screen in a production React app, you know the specific flavor of dread: React’s failure mode for an unhandled render error isn’t a helpful message, it’s nothing. The framework unmounts the tree and leaves you a blank canvas and your thoughts.

That incident is why I now keep a mental ranked list of causes. The white screen has maybe four common parents, and they announce themselves differently if you know where to look.

The four usual suspects, ranked

First: a render error with no error boundary. Some component throws during render — a cannot read properties of undefined on data that was shaped differently than expected, usually — and because nothing above it catches the error, React unmounts everything. The entire app. This is documented, intentional behavior: React would rather show nothing than show corrupted UI. I understand the reasoning and I still think the default punishes exactly the teams who haven’t learned about error boundaries yet, which is to say, the teams who need mercy most.

Second: a chunk load failure. Your app is code-split, a user has a tab open from before your deploy, they click a route, and the browser requests a chunk file that no longer exists on your CDN. ChunkLoadError: Loading chunk 42 failed. If that lazy route is wrapped in Suspense with no error handling, or the failure happens at the top of the tree, the result is a white page. This one is sneaky because it’s not a bug in any meaningful sense — every line of your code is correct — it’s a deployment lifecycle problem. It deserves its own discussion, and I wrote one: chunk load errors after deploy.

Third: a hydration crash taking down the root. In an SSR framework, the server sends real HTML, so the user briefly sees a full page. Then hydration runs, hits a fatal mismatch or a render error, and React tears down what the server built. The tell is in the timing: the user reports the page appeared and then vanished. A plain white screen from first paint points elsewhere; a flash of content followed by white is hydration eating your page in front of the user.

Fourth: a bad deploy serving mismatched assets. The HTML from the new deploy references main.abc123.js, but a stale CDN edge or a half-finished upload serves the old bundle — or serves your 404 page as JavaScript with a 200 status, which produces the unforgettable Uncaught SyntaxError: Unexpected token '<'. The page is white because the app never booted at all. This is the rarest of the four but the most embarrassing, because the fix is usually “invalidate the cache and upload atomically,” which someone proposed six months ago in a ticket nobody prioritized.

How to debug a white screen in production React

Here’s the sequence when all you have is a blank page and a stressed user on the phone. The goal is to classify the failure before you touch any code.

Ask the user to open the page and tell you whether anything flashes before the white. Content-then-white means hydration or a render error on mount. White-from-the-start means the bundle never ran: chunk failure, asset mismatch, or a syntax-level crash.

Check your error tracking, obviously — but check it for the absence of errors too. A render error with no boundary still fires window.onerror, so it should be captured. A page that’s white with zero reported errors is strong evidence the JavaScript never executed, which pushes you toward the asset-mismatch theory. Silence is a clue.

Then reproduce their environment, not yours. Same route, same account if you can, and critically: a stale tab. Open the app, deploy nothing, wait, then navigate — most engineers test the fresh-load path a hundred times and the stale-tab path never, which is exactly why chunk errors live so long in production.

The step that has saved me the most hours: watch what the user actually did. In the Friday incident I mentioned, we burned forty minutes on theories before a session replay showed the user had opened the page from a two-day-old tab pinned in their browser, clicked into settings, and hit a chunk that our deploy had deleted. Console and network capture in the same timeline meant the ChunkLoadError was sitting right there next to the failed request. No theory needed. The fix was boring; finding it without the recording would not have been.

Error boundaries: placement is the whole game

Everyone knows error boundaries exist. The mistake is deploying exactly one, at the root, and calling it a day. A root boundary converts “white screen” into “full-page error message,” which is better, but it still takes down your entire app because one widget in the sidebar threw.

The strategy that works: boundaries at feature seams, so a crash is contained to the region that caused it. Route level, then around independent panels — anything that could fail without making the rest of the page useless.

class ErrorBoundary extends React.Component {
  state = { error: null }
  static getDerivedStateFromError(error) {
    return { error }
  }
  componentDidCatch(error, info) {
    reportError(error, { componentStack: info.componentStack })
  }
  render() {
    if (this.state.error) {
      return this.props.fallback ?? <div>This section failed to load.</div>
    }
    return this.props.children
  }
}

// Root: last line of defense, full-page apology
// Route: contains navigation-level failures
// Widget: sidebar dies, dashboard lives
<ErrorBoundary fallback={<FullPageError />}>
  <ErrorBoundary fallback={<RouteError />}>
    <Dashboard>
      <ErrorBoundary fallback={<PanelError />}>
        <ActivityFeed />
      </ErrorBoundary>
    </Dashboard>
  </ErrorBoundary>
</ErrorBoundary>

Yes, it’s still a class component in 2026 — getDerivedStateFromError has no hook equivalent, so this is the one class you keep. Write it once, or use react-error-boundary and move on.

Two placement rules I hold with actual conviction. One: every React.lazy gets a boundary that specifically handles chunk failures, because those will happen and the right response (offer a reload) differs from a code crash (apologize and report). Two: the fallback UI must be dumber than the thing it replaces. I’ve seen a fallback component that fetched data to render a nicer error message, then threw during that render. An error boundary whose fallback crashes is a philosophical joke that your users don’t find funny.

One caveat so you’re not surprised at 4:50 on a Friday: boundaries only catch errors during rendering, lifecycle methods, and constructors. Errors in event handlers, setTimeout callbacks, and rejected promises sail right past them. Those won’t white-screen you — React doesn’t unmount for errors outside render — but they also won’t hit your boundary’s reporting, so you still need global onerror and onunhandledrejection handlers for full coverage.

The white screen feels like a mystery every time, but it’s a short suspect list. Classify first — did the bundle run, did content flash, is error tracking silent — and the blank page starts talking.

See the bug the way your user did

LogReplay captures session replays, console output, network requests, and errors in one timeline — so you stop guessing what happened before the ticket arrived.

Try LogReplay free