When using React.lazy() for code splitting, errors during chunk loading or component initialization are not caught unless wrapped in an Error Boundary. This leads to unhandled promise rejections and white-screen failures.
React.lazy() enables code splitting by asynchronously loading component chunks. When the Promise rejects—due to network failures, missing chunks, or component errors—React throws the error for the nearest Error Boundary to handle. Without an Error Boundary, unhandled errors will crash your application with no graceful fallback. Error Boundaries are class components (or hooks wrappers) that catch errors thrown in child components. For lazy-loaded components, they catch both chunk-loading failures and rendering errors that occur after the component loads.
Error Boundaries must be class components. Create one to wrap your lazy components:
import React from 'react';
interface ErrorBoundaryProps {
children: React.ReactNode;
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div style={{ padding: '20px' }}>
<h2>Something went wrong</h2>
<p>{this.state.error?.message || 'Failed to load this component'}</p>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;Always place ErrorBoundary outside (above) your Suspense component to catch both loading and rendering errors:
import React, { Suspense } from 'react';
import ErrorBoundary from './ErrorBoundary';
// Lazy load a component
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));
function App() {
return (
<ErrorBoundary>
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
</ErrorBoundary>
);
}
export default App;The nesting order is critical: ErrorBoundary → Suspense → Lazy component.
You can create a specialized error boundary for lazy routes that provides better feedback:
class LazyErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error) {
// Check if this is a chunk loading error
const isChunkLoadError = /loading chunk/.test(error.message);
return { hasError: true, error, isChunkLoadError };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
if (/loading chunk/.test(error.message)) {
console.error('Chunk loading failed:', error);
} else {
console.error('Component error:', error);
}
}
render() {
if (this.state.hasError) {
return (
<div style={{ padding: '40px', textAlign: 'center' }}>
<h2>Failed to load this page</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => window.location.reload()}>
Reload page
</button>
</div>
);
}
return this.props.children;
}
}Instead of one global error boundary, create multiple boundaries at different levels to isolate failures:
function Dashboard() {
const UserPanel = React.lazy(() => import('./UserPanel'));
const Analytics = React.lazy(() => import('./Analytics'));
const Settings = React.lazy(() => import('./Settings'));
return (
<div>
<header>My Dashboard</header>
<ErrorBoundary>
<Suspense fallback={<Skeleton />}>
<UserPanel />
</Suspense>
</ErrorBoundary>
<ErrorBoundary>
<Suspense fallback={<LoadingSpinner />}>
<Analytics />
</Suspense>
</ErrorBoundary>
<ErrorBoundary>
<Suspense fallback={<LoadingSpinner />}>
<Settings />
</Suspense>
</ErrorBoundary>
</div>
);
}This way, if one lazy section fails, the others still render normally.
Test your error boundary during development to ensure it catches errors properly:
// In your lazy component, simulate an error
const HeavyComponent = () => {
// Force an error during development
if (process.env.NODE_ENV === 'development' && Math.random() > 0.99) {
throw new Error('Test error from lazy component');
}
return <div>Component loaded successfully</div>;
};
export default HeavyComponent;
// Or test the boundary with this pattern
function TestError() {
const [shouldError, setShouldError] = React.useState(false);
if (shouldError) {
throw new Error('Intentional test error');
}
return (
<div>
<button onClick={() => setShouldError(true)}>
Trigger Error
</button>
</div>
);
}Multiple Error Boundaries: You can nest error boundaries at different tree levels. An inner boundary will catch errors within its scope, allowing outer boundaries to catch errors from other parts of the app. This is useful for large applications where you want granular error handling.
Chunk Loading Errors vs Component Errors: Network failures that prevent chunks from loading and errors thrown after a component renders are both caught by Error Boundaries. You can distinguish between them in componentDidCatch by checking the error message for patterns like "loading chunk" or "Failed to fetch dynamically imported module".
What Error Boundaries Don't Catch: Error boundaries do NOT catch:
- Errors in event handlers (use try-catch in handlers)
- Asynchronous code (use try-catch with async/await)
- Server-side rendering errors
- Errors in the Error Boundary itself
- Errors in setTimeout/setInterval callbacks
React Router Integration: When using React Router with lazy route components, place ErrorBoundary at the route level:
const routes = [
{
path: '/dashboard',
element: (
<ErrorBoundary>
<Dashboard />
</ErrorBoundary>
),
},
];Fallback UI vs Error Boundary: Suspense.fallback shows loading UI while the chunk is being fetched. Error Boundary shows error UI if the fetch fails. Both are needed for complete coverage.
Prop spreading could cause security issues
Prop spreading could cause security issues
Error: error:0308010C:digital envelope routines::unsupported
Error: error:0308010C:digital envelope routines::unsupported
React Hook "useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render.
React Hook useEffect placed inside a condition
Hook can only be called inside the body of a function component
Hook can only be called inside the body of a function component
Rollup failed to resolve import during build
How to fix "Rollup failed to resolve import" in React