GrabDiff
← Blog

Why your React app can go down and your uptime monitor won't know

May 2026 ·

There's a category of React production incident that's particularly insidious: the blank white screen that your uptime monitor never catches.

The server is up. The CDN is responding. The HTML file is being delivered. From the perspective of any HTTP-based monitor, everything is working perfectly. HTTP 200 across the board. Response times nominal.

Meanwhile, every user who loads your app is looking at an empty white screen.


Why React apps fail invisibly

A React app served as a single-page application sends an HTML file that looks roughly like this:

<!DOCTYPE html>
<html>
  <head>
    <title>My App</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="/static/js/main.abc123.js"></script>
  </body>
</html>

That's what the server sends. That's what returns HTTP 200. The actual content — everything users see — is rendered by the JavaScript bundle after the page loads in the browser.

When that JavaScript throws an unhandled exception before it mounts the React tree, you get a blank #root div. The HTML file was valid. The server was healthy. Your monitor saw 200. But there's nothing on the page.

Common causes I've seen

Undefined environment variables. process.env.REACT_APP_API_URL was set in your .env and in staging. Someone didn't add it to the production deployment config. The JavaScript crashes trying to construct a URL from undefined.

A bad dependency update. A package update changed an API in a way that broke your code, but the tests didn't catch it because the affected component isn't well-covered. Deploys fine, fails at runtime.

A missing chunk. Code splitting with React.lazy() means your app loads additional JavaScript bundles on demand. If a bundle 404s — old CDN cache pointing to a hash that no longer exists after a new deploy — the component that needs it throws, crashes the tree, blank screen.

An error boundary masking the problem. You have error boundaries, which is good practice. But a poorly configured boundary catches the crash and renders nothing instead of a useful error message. The page isn't blank anymore, it's just empty — harder to notice, easier to miss.

Next.js has its own flavor of this

Next.js apps using server-side rendering (SSR) or static site generation (SSG) have slightly different failure modes, but the core problem is the same.

With SSR: if getServerSideProps throws, Next.js typically returns a 500. But if the error happens during client-side hydration after the initial server-rendered HTML is delivered, you get the blank screen problem on the client side, with a 200 from the server.

With ISR (Incremental Static Regeneration): pages are cached and served statically. If a revalidation build fails, Next.js falls back to serving the stale cached version — which could be hours or days old. The server returns 200. The content is stale. Whether this matters depends on your app.

How to actually detect this

The only reliable way to detect a blank screen or a crashed React app is to load the page in a real browser and look at what rendered.

This means running headless Chrome or similar, navigating to the URL, waiting for JavaScript execution to complete, and taking a screenshot. If the screenshot shows a blank page, your app is broken. If it shows the expected content, things are fine.

This is exactly what visual uptime monitoring does. GrabDiff runs headless Chrome on a schedule, takes a full-page screenshot of your URL, and diffs it against a baseline you set when everything was known to be working. A blank screen will differ dramatically from the baseline. You get an alert immediately, with the diff image attached, so you can see exactly what changed.

Add it alongside your existing HTTP monitor. The HTTP check catches hard failures fast. The visual check catches the silent failures that HTTP 200 hides.