How to Fix the White Screen of Death in React

How do I fix the white screen of death in React?

TL;DR

Constraints

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 componentDidCatchfetch() 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

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