How to Fix the White Screen of Death in React
How do I fix the white screen of death in React?
TL;DR
- Bottom line: React's WSOD is almost always a JS error that crashes before render. Top 5
causes: (1) unhandled render exception, (2) wrong
homepage/baseURL, (3) missing Error Boundary, (4) broken import/export, (5) React Router misconfiguration. - Key tool/command: Open DevTools Console (F12) — the error is always logged there.
- Watch out for: Production builds hide details. Works in dev but blank in prod = base URL / routing issue.
- Works with: React 16-19+, CRA, Vite, Next.js, any React bundler.
Constraints
- Error Boundaries only catch errors in rendering, lifecycle methods, and constructors — NOT event handlers, async code, or SSR. For those, use try/catch.
- React 19 removed
ReactDOM.render()— usecreateRoot()or the app will silently fail to mount. [src7] - Never suppress console errors to "fix" WSOD — the console error IS the diagnostic. Always read it first. [src5]
- Next.js app directory uses its own
error.tsxconvention — do not use class-based Error Boundaries there. [src1] - Production builds minify React error messages — deploy source maps or use Sentry for readable stack traces.
Quick Reference
| # | Cause | Likelihood | Signature | Fix |
|---|---|---|---|---|
| 1 | Unhandled render error | ~30% | Works in dev, blank in prod | Add Error Boundary [src1] |
| 2 | Wrong homepage/base |
~25% | Assets return 404 in prod | Set correct homepage or base [src2, src3] |
| 3 | Broken import/export | ~15% | "X is not a component" error | Fix default/named mismatch [src5] |
| 4 | React Router on static host | ~10% | Root works, routes 404 on refresh | SPA fallback redirects [src4] |
| 5 | Async data crash | ~8% | Renders then goes blank | Optional chaining + loading state [src1] |
| 6 | Missing return in component | ~5% | No error, just blank | Ensure JSX is returned [src5] |
| 7 | CORS/network error | ~4% | Blank + console errors | Fix API CORS or proxy config [src5] |
| 8 | Minification/case error | ~3% | Works in dev, crashes in prod | Check case-sensitivity in imports [src6] |
| 9 | ReactDOM.render() in React 19 |
New in 2025 | App silently fails to mount | Migrate to createRoot() [src7] |
Decision Tree
START
├── Open DevTools Console (F12) — any errors?
│ ├── YES → Read the error message
│ │ ├── "X is not defined" → Missing import or export [src5]
│ │ ├── "Cannot read properties of undefined/null" → Data not loaded [src1]
│ │ │ └── FIX: Add optional chaining (data?.field) or loading state
│ │ ├── "Element type is invalid" → Import/export mismatch [src5]
│ │ │ └── FIX: Check default vs named exports
│ │ ├── "Loading chunk failed" → Code splitting error [src6]
│ │ │ └── FIX: Clear cache, check CDN, add Suspense fallback
│ │ ├── "createRoot is not a function" → React 19 API change [src7]
│ │ │ └── FIX: import { createRoot } from 'react-dom/client'
│ │ └── Other JS error → Fix the specific error
│ └── NO errors? → Check Network tab (F12 > Network)
│ ├── JS/CSS returning 404 → Wrong base URL [src2, src3]
│ │ └── FIX: Set homepage or base to correct path
│ ├── API calls failing → Backend issue (CORS, auth, URL)
│ └── All 200 OK → Check if root <div id="root"> exists
├── Works in dev but not prod?
│ ├── Routes work at root but 404 on refresh → Routing [src4]
│ │ └── FIX: Configure server for SPA fallback
│ ├── Blank on all routes → base URL or build issue
│ └── React 19 migration? → Check for removed APIs [src7]
└── DEFAULT → Add Error Boundary to catch and display errors [src1]
Decision Logic
If the browser console shows a JavaScript error
Read the error message verbatim — that error IS the diagnostic. Open DevTools (F12) > Console, fix the specific error before doing anything else. [src5]
If the app works in dev (npm start) but blank in production
Check homepage (CRA) or base (Vite) in your config first — wrong base URL accounts for ~25% of WSOD cases. [src2, src3]
If a route works at / but returns 404 on refresh
Configure SPA fallback on your static host (_redirects for Netlify, vercel.json for Vercel, try_files for nginx). React Router cannot recover from a 404 server response. [src4]
If you migrated to React 19+ and the app silently fails to mount
Replace ReactDOM.render() with createRoot() from react-dom/client. The legacy API was removed in 19.0 and still ships in many tutorials. [src7]
If lazy-loaded chunks intermittently fail after a deploy
Wrap React.lazy() calls in a retry wrapper with cache busting and ensure every lazy component is inside <Suspense fallback={...}>. Stale CDN chunks are a top-3 2026 cause. [src1, src9]
If you need error reporting that survives page unload
Use navigator.sendBeacon('/api/errors', payload) in componentDidCatch — fetch() is killed when the user closes the tab, sendBeacon is not.
If you've added Error Boundaries and still see WSOD on event-handler errors
Error Boundaries do NOT catch event-handler, async, or SSR errors. Wrap async handlers in try/catch and call setError(err) to push the error into the boundary, or use the react-error-boundary library's useErrorBoundary hook. [src1, src8]
Default
Add a top-level Error Boundary, set onUncaughtError on createRoot, and ship source maps. WSOD without diagnostics is a 30-minute fix that becomes a 30-hour fix without these three. [src1, src7]
Step-by-Step Guide
1. Check the browser console (F12)
Always start here. The error that killed the render is logged. [src5]
# Common console errors that cause WSOD:
TypeError: Cannot read properties of undefined (reading 'map')
ReferenceError: X is not defined
Error: Element type is invalid: expected a string... but got undefined
ChunkLoadError: Loading chunk 5 failed
Verify: Open F12 > Console tab. If errors appear, read them before trying anything else.
2. Add an Error Boundary
Error Boundaries catch render errors and show fallback UI. [src1]
// ErrorBoundary.jsx
import { Component } from 'react';
class ErrorBoundary extends Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('React Error Boundary caught:', error, errorInfo);
// Send to Sentry, etc.
}
render() {
if (this.state.hasError) {
return (
<div style={{ padding: '2rem', textAlign: 'center' }}>
<h1>Something went wrong</h1>
<pre style={{ color: 'red' }}>{this.state.error?.message}</pre>
<button onClick={() => window.location.reload()}>Reload</button>
</div>
);
}
return this.props.children;
}
}
// Wrap your app:
// <ErrorBoundary><App /></ErrorBoundary>
Verify: Intentionally throw an error inside a component — you should see fallback UI, not blank.
3. Fix base URL for production
// package.json (Create React App)
{
"homepage": "https://myapp.com"
}
// Or for subdirectory:
{
"homepage": "/my-app"
}
// vite.config.js (Vite) [src3]
export default defineConfig({
base: '/', // or '/my-app/' for subdirectory
});
Verify: npx serve -s build (CRA) or npx serve -s dist (Vite) — page should render locally.
4. Fix React Router for static hosting
# Netlify: create public/_redirects
/* /index.html 200
# Vercel: create vercel.json
{ "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] }
# Nginx:
location / {
try_files $uri $uri/ /index.html;
}
# Apache: create public/.htaccess
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
Verify: Navigate to a deep route, then refresh — the page should render, not 404.
5. Migrate to createRoot (React 19)
React 19 removed the legacy render API. [src7]
// ❌ React 18 and earlier (removed in React 19)
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));
// ✅ React 19+ (required)
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
Verify: npm run build && npx serve -s dist — app should mount without errors.
6. Configure React 19 error handlers on createRoot
React 19 adds onCaughtError, onUncaughtError, and onRecoverableError. [src7]
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'), {
onCaughtError: (error, errorInfo) => {
// Sent to Error Boundary — log for analytics
console.warn('Caught by boundary:', error.message);
},
onUncaughtError: (error, errorInfo) => {
// NOT caught by any boundary — critical
reportToSentry(error, errorInfo);
},
onRecoverableError: (error) => {
// React recovered automatically
console.info('Recovered:', error.message);
},
});
root.render(<App />);
Code Examples
React: Complete Error Boundary with reset capability
Full script: react-complete-error-boundary-with-reset-capabilit.jsx (65 lines)
// Input: Any React app that crashes with white screen
// Output: Graceful error UI with retry capability
import { Component } from 'react';
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
// ... (see full script)
React 19: Using react-error-boundary library (functional approach)
// Input: React 19 app that needs error boundaries without class components
// Output: Functional error boundary with automatic reset on navigation
import { ErrorBoundary } from 'react-error-boundary'; // v5.0+
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert" style={{ padding: '2rem' }}>
<h2>Something went wrong</h2>
<pre style={{ color: 'red' }}>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
// Wrap your app or individual routes:
// <ErrorBoundary FallbackComponent={ErrorFallback}
// onReset={() => window.location.reload()}
// onError={(error, info) => logErrorToService(error, info)}>
// <App />
// </ErrorBoundary>
React 19.2: Lazy-load retry wrapper for stale chunks after deploy
// Input: React.lazy() that intermittently fails with ChunkLoadError after deploys
// Output: Auto-retry + cache clear + reload on persistent failure
import { lazy } from 'react';
function lazyWithRetry(importFn, retries = 2) {
return lazy(async () => {
try {
return await importFn();
} catch (err) {
if (retries > 0 && /ChunkLoadError|Loading chunk/.test(err.message)) {
// Clear stale chunks and retry once
if ('caches' in window) {
const keys = await caches.keys();
await Promise.all(keys.map((k) => caches.delete(k)));
}
return lazyWithRetry(importFn, retries - 1)._payload._result;
}
// Final fallback: hard reload to fetch latest index.html
window.location.reload();
throw err;
}
});
}
const Dashboard = lazyWithRetry(() => import('./Dashboard'));
// <Suspense fallback={<Loading />}><Dashboard /></Suspense>
Anti-Patterns
Wrong: No error handling in data rendering
// ❌ BAD — crashes with WSOD if data is null/undefined [src1]
function UserProfile({ user }) {
return <h1>{user.name}</h1>; // TypeError if user is undefined
}
Correct: Null checks and loading states
// ✅ GOOD — handles loading and null states [src1]
function UserProfile({ user }) {
if (!user) return <p>Loading...</p>;
return <h1>{user.name}</h1>;
}
Wrong: Mismatched default/named exports
// ❌ BAD — component.jsx exports named, imported as default
// component.jsx:
export function MyComponent() { return <div>Hi</div>; }
// app.jsx:
import MyComponent from './component'; // WRONG — undefined!
Correct: Match import style to export
// ✅ GOOD — consistent exports [src5]
// Option A: named export + named import
export function MyComponent() { return <div>Hi</div>; }
import { MyComponent } from './component';
// Option B: default export + default import
export default function MyComponent() { return <div>Hi</div>; }
import MyComponent from './component';
Wrong: Using removed ReactDOM.render() in React 19
// ❌ BAD — silently fails in React 19, app never mounts [src7]
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));
Correct: Using createRoot API
// ✅ GOOD — required for React 19 [src7]
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
Common Pitfalls
- Case-sensitive imports on Linux:
import Header from './header'works on macOS/Windows but fails on Linux if file isHeader.jsx. Fix: match exact case. [src6] - Missing Suspense for lazy components:
React.lazy()without<Suspense>crashes silently. Fix: always wrap lazy components. [src1] - Environment variables not prefixed: CRA needs
REACT_APP_, Vite needsVITE_. Without prefix, they're undefined. [src2, src3] - StrictMode double render: React StrictMode renders twice in dev, exposing race conditions. Fix: ensure components handle double-render. [src1]
- Build path mismatch: CRA outputs to
build/, Vite todist/. Deploying wrong folder = blank page. [src2, src3] - React 19 error log deduplication: React 19 logs errors once instead of three times. If your monitoring relies on counting console.error calls, update tooling. [src7]
- React Router v7 fog of war: Deferred route manifests can cause blank pages if the manifest fails to load. Fix: ensure server correctly serves route manifest files. [src4]
- Stale lazy chunks after deploy: Browsers with the old
index.htmlcached try to fetch chunk hashes that no longer exist. Fix: wrapReact.lazy()in a retry-with-reload wrapper (catchChunkLoadError, clearcaches,location.reload(true)). - React 19.2 Compiler skips violations silently: As of React 19.2 (October 2025), the
Compiler is enabled by default in modern Vite/CRA setups and automatically skips components with
violations rather than failing the build. A component with rules-of-hooks violations may render in dev
(un-optimized) but crash in production once a different code path runs. Fix: enable
eslint-plugin-react-hookswith the new compiler-powered rules and treat warnings as errors. [src9] - Error reporting lost on tab close: A
fetch('/api/errors', ...)call incomponentDidCatchis killed if the user closes the tab before it completes. Fix: usenavigator.sendBeacon('/api/errors', JSON.stringify(payload))— guaranteed to fire even on unload.
Diagnostic Commands
# Check for build errors
npm run build 2>&1 | tail -20
# Serve production build locally to reproduce
npx serve -s build # CRA
npx serve -s dist # Vite
# Check if index.html has correct script paths
cat build/index.html | grep -o 'src="[^"]*"'
# Verify base URL in built assets
grep -r 'base' vite.config.* 2>/dev/null
grep '"homepage"' package.json 2>/dev/null
# Check for case-sensitivity issues
find src -name "*.jsx" -o -name "*.tsx" | sort
# Check React version (is it 19+?)
npm ls react | head -5
# Check for removed React 19 APIs in codebase
grep -r "ReactDOM.render\|unmountComponentAtNode" src/ --include="*.{js,jsx,ts,tsx}"
Version History & Compatibility
| React Version | Status | Changes Relevant to WSOD |
|---|---|---|
| React 16+ | LTS (16.x EOL) | Error Boundaries introduced [src1] |
| React 18 | Maintained | Strict Mode double-render in dev; createRoot() introduced as opt-in [src7] |
| React 19 (Dec 2024) | LTS | ReactDOM.render() removed; single error log; onCaughtError /
onUncaughtError callbacks [src7] |
| React 19.2 (Oct 2025) | Current | React Compiler enabled by default in Vite/CRA; Compiler skips violation components silently (show in dev but can crash in prod if untested paths run); error-handling APIs unchanged from 19.0; improved component stacks [src9] |
When to Use / When Not to Use
| Use When | Don't Use When | Use Instead |
|---|---|---|
| App shows blank white page | Page shows but layout broken | CSS debugging (inspect element styles) |
| Console shows JavaScript errors | Server returns 500 | Backend/server debugging |
| Works in dev, blank in prod | 404 on page load (HTML not served) | Hosting/server configuration |
| React app not rendering at all | React Native white screen | React Native WSOD debugging |
Important Caveats
- Error Boundaries only catch errors in rendering, lifecycle methods, and constructors — NOT in event handlers, async code, or SSR. Use try/catch for those.
- Production builds minify React error messages. Use source maps or Sentry for readable stack traces.
- Next.js has its own error handling (
error.tsx,_error.tsx) — don't use class-based Error Boundaries in Next.js app directory. - React 19's
onUncaughtErrorandonCaughtErrorare set oncreateRoot, not on individual components — they're app-level handlers. [src7] - The
react-error-boundarylibrary (v5+) provides a functional API that works with React 19 and avoids class component boilerplate. [src8]