How to Debug a Blank Page (White Screen) in a Web App
The white screen of death is rarely silent. An uncaught render error unmounts the whole React tree to an empty root div. Open the Console first, walk a four-branch decision tree, and each branch ends in one concrete fix.

The white screen of death feels like the app told you nothing. It told you plenty; you just have to open the right tab. A blank page is a symptom with a short list of causes, and the fastest engineers do not stare at the empty viewport — they read the Console, classify the failure in one of four buckets, and apply the fix that bucket demands.
This is a decision tree, not a checklist of vibes. We start at the single most diagnostic place (the DevTools Console), branch on what it shows, and end every branch in a concrete change. The last section is the part most "fix the white screen" articles skip: turning the silent thrown error into an artifact an AI agent can read, so the fix gets drafted from the actual stack trace instead of re-derived from a screenshot of nothing.
Why does a blank page happen at all?
A blank page happens because an uncaught error during render tears down the UI. React's documentation states that as of React 16, an error not caught by any error boundary unmounts the entire component tree, leaving an empty root element. React chose this because a corrupted interface — a wrong payment amount, a message to the wrong person — is more dangerous than no interface. The console almost always still holds the thrown error.
The legacy React docs are blunt about it: "As of React 16, errors that were not caught by any error boundary will result in unmounting of the whole React component tree." The rationale is a deliberate safety trade. The team decided that leaving a corrupted UI on screen is worse than removing it — a Messenger bug that routes a message to the wrong person, or a payments screen that renders the wrong total, does more harm than a blank panel. So when render throws and nothing catches it, React clears the DOM. The empty page is the safety mechanism doing its job, not a mystery.
Which means the error did not vanish. It was logged on its way out. MDN documents that console.error() outputs "at the 'error' log level" and that the message "may be formatted as an error, with red colors and call stack information" — making the DevTools Console the first and best place to find the throw behind the blank render. An uncaught synchronous error also surfaces on the window error event. Start there.
The decision tree: four branches from the Console
Open DevTools, go to the Console, reload the page, and read the first red line. Where you land next depends entirely on what you see. The four branches below are mutually exclusive enough to triage in under a minute.
- A thrown error with a stack trace. Read the top frame — it names the file and line that threw. Fix that component, then wrap it in an error boundary so the next crash degrades to a fallback instead of a blank page.
- A hydration warning ("Text content does not match server-rendered HTML" / "Hydration failed"). The server and client rendered different markup. Move client-only values into
useEffect, or render that subtree client-side only. - A clean Console but still blank. Switch to the Network tab. A JavaScript chunk returning 404 or 500 means a script never loaded, so nothing could mount. This is a build, path, or deploy problem.
- Bundle loaded, nothing mounted. Confirm the root element id matches what
createRoottargets, and that your router actually matched a route rather than rendering an empty default.
Branch 1: the thrown error, and the error boundary that should have caught it
An error boundary contains a render crash to a fallback so the rest of the tree survives. It is a class component implementing static getDerivedStateFromError (to render the fallback) and optionally componentDidCatch (to log). Function components cannot be error boundaries today, which trips up plenty of teams expecting a hook to exist.
But boundaries have hard limits, and this is the single most common reason one "isn't working." The react.dev Component reference lists four error classes error boundaries do not catch: event handlers, server-side rendering, errors thrown in the boundary itself, and asynchronous code such as setTimeout or requestAnimationFrame callbacks. So a throw inside an onClick or an async resolver slips straight past. For those, MDN notes the window error event "is only generated for script errors thrown synchronously" — an uncaught throw inside an async function with no rejection handler fires unhandledrejection instead, so it may never hit your error handler at all.
import { Component, type ReactNode } from "react";
class ErrorBoundary extends Component<
{ children: ReactNode; fallback: ReactNode },
{ hasError: boolean }
> {
state = { hasError: false };
// Renders the fallback instead of blanking the whole tree.
static getDerivedStateFromError() {
return { hasError: true };
}
// Boundaries DO catch render/lifecycle throws — log them here.
componentDidCatch(error: Error) {
reportToBugTracker(error);
}
render() {
return this.state.hasError ? this.props.fallback : this.props.children;
}
}
// Boundaries do NOT catch: event handlers, SSR, async callbacks,
// or throws inside the boundary itself. Cover those two cases manually:
window.addEventListener("unhandledrejection", (e) => reportToBugTracker(e.reason));
function onClick() {
try { mightThrow(); } catch (err) { reportToBugTracker(err as Error); }
}Branch 2: the hydration mismatch
If the Console shows a hydration warning, the server-rendered HTML and the first client render disagreed. The Next.js error page enumerates seven distinct causes: incorrect HTML nesting (a <div> inside a <p>), typeof window checks in render, browser-only APIs like window or localStorage during render, time-dependent APIs such as the Date() constructor during render, browser extensions mutating the HTML, misconfigured CSS-in-JS, and a misconfigured Edge/CDN that rewrites the response.
Why this can blank the page: react.dev states React "will not attempt to patch mismatched text content," and that if you call root.render before hydration finishes, React "will clear the existing server-rendered HTML content and switch the entire root to client rendering" — a common blank-flash path. The standard fix is to defer client-only values until after mount, so the first client render matches the server.
import { useEffect, useState } from "react";
// Anything that reads window / Date() / localStorage must wait until
// after mount, so the server render and first client render agree.
export function LastSeen() {
const [isClient, setIsClient] = useState(false);
useEffect(() => setIsClient(true), []); // runs only on the client
// Server and first client render are identical -> no mismatch.
return <span>{isClient ? new Date().toLocaleString() : "…"}</span>;
}Those two integers are worth internalizing. Seven enumerated hydration triggers and four documented error-boundary blind spots cover the large majority of "my page is blank" reports. Memorize the buckets and the diagnosis stops being archaeology.
Branch 3 & 4: failed bundle, or nothing mounted
If the Console is clean but the page is still empty, the JavaScript may never have run. Open the Network tab and filter to JS. A .js chunk returning 404 (wrong path, bad publicPath, a deploy that moved hashed filenames) or 500 (a server or edge function erroring) means nothing could mount, because the code that renders your app never arrived. This is the branch that explains "works locally, blank in production": development serves chunks from one origin, production from a CDN, and a single misconfigured base path breaks every import.
If the bundle did load and you still see nothing, the mount target is wrong. Confirm the element id passed to createRoot(document.getElementById("root")) actually exists in your HTML, and that your router matched the current URL rather than falling through to an empty default route. A typo between root and app is a surprisingly frequent cause of a perfectly healthy bundle rendering into the void.
The wedge: hand the silent error to an AI agent
Here is where the standard playbook stops. Read the Console, branch, fix — that loop assumes a human is sitting at the failing machine with DevTools open. The hard version of a blank page is the one a teammate or a user hits that you cannot reproduce: they send a screenshot of an empty page, and the red error that explains everything died in their Console, never yours.
That is the gap BugMojo closes. The browser extension captures the white-screen moment as a structured artifact: the rrweb DOM replay, the full console including the red thrown error the blank render swallowed, and the Network tab showing the 404 chunk — all at the instant the screen went blank. It then exposes that capture over an MCP server, so an AI coding agent (Claude Code, Cursor) reads the actual stack trace and the failing request and drafts the fix. No human re-derives "works on my machine" from a picture of nothing.
| Feature | Manual DevTools | Prod error monitor (e.g. Sentry) | BugMojo |
|---|---|---|---|
| Shows the thrown error behind the blank page | only on your machine | ✓ | ✓ |
| Captures the white-screen moment with one click | — | auto, server-side | ✓ |
| rrweb DOM replay + console + network on one timeline | — | partial | ✓ |
| Works with zero project setup (Quick Capture) | ✓ | — | ✓ |
| Mature production error monitoring at scale | — | ✓ | early |
| Source-map symbolication of minified prod codes | manual decoder | ✓ | basic |
| Capture exposed to an AI agent over MCP | — | — | ✓ |
Read that table both directions honestly. If you need production exception monitoring at scale with alerting and deep source-map symbolication, a dedicated error monitor is more mature and you should run one. BugMojo's uncontested row is the last one: the capture of the silent console error, the replay, and the failing request, handed to an agent as evidence it can reason over — not a screenshot of an empty viewport. That is the difference between an agent guessing and an agent reading the stack trace.
A blank page is not the absence of information. It is information you have not opened the right tab to read yet.BugMojo engineering
Install the free BugMojo extension to capture the blank-page moment — rrweb replay, the full console (including the thrown error), and the failed network request — then let an AI agent read the stack trace and draft the fix over MCP. No project setup required.
Install the extensionFrequently asked questions
Frequently asked questions
Sources
- Component – React reference (getDerivedStateFromError, componentDidCatch, and the four error classes error boundaries do NOT catch) — React (Meta) (2025)
- hydrateRoot – React (React will not patch mismatched text; root.render before hydration clears server HTML and switches the root to client rendering) — React (Meta) (2025)
- Text content does not match server-rendered HTML – Next.js (7 enumerated hydration-mismatch causes and fixes) — Vercel / Next.js (2025)
- console: error() static method – Web APIs | MDN (error log level, red colors, call stack information) — MDN Web Docs (Mozilla) (2025-07-04)
- Window: error event – Web APIs | MDN (synchronous script errors vs unhandledrejection for async throws) — MDN Web Docs (Mozilla) (2025-11-03)
- Error Boundaries – React (legacy docs: uncaught errors unmount the whole tree as of React 16; corrupted-UI rationale) — React (Meta) (2022)
Get bug-tracking insights, weekly.
Engineering deep-dives, QA playbooks, and honest tool comparisons. No spam — unsubscribe in one click.

