The Error
You just deployed. Users start reporting a blank page or a broken feature. The console shows:
ChunkLoadError: Loading chunk 3 failed.
(error: https://example.com/static/js/3.chunk.js)
Sometimes it's chunk 3, sometimes chunk 7, sometimes a named chunk like vendors~main. The browser tried to fetch a JavaScript file and got a 404, a network error, or a cached pointer to a file that no longer exists.
Root Causes
Three things cause this error, and one of them almost certainly explains what you're seeing:
- Stale HTML referencing old chunk filenames โ webpack content-hashes change on every deploy. If a user's browser (or your CDN edge) is still serving the old
index.html, it'll request chunk filenames from the previous build โ files that are now gone. - Chunks missing from the server (404) โ the deployment failed silently, or the wrong build folder got uploaded. The HTML loaded fine, but the
.chunk.jsit references simply isn't there. - Wrong public path for dynamic imports โ a
React.lazy()orimport()call resolves to a path that doesn't exist in production. WrongPUBLIC_URL, subdirectory mismatch, CDN misconfiguration โ any of these will do it.
Step 1 โ Verify the Chunk Actually Exists
Start with the simplest check. Open the URL from the error message directly in your browser, or curl it:
curl -I https://example.com/static/js/3.chunk.js
A 404 Not Found means the file was never deployed or got overwritten. Check your local build output:
ls -la build/static/js/ | grep chunk
File exists locally but not on the server? Your deployment pipeline has a gap โ the upload step missed something. Fix it and redeploy completely before touching anything else.
Step 2 โ Fix Stale HTML (The Most Common Cause)
Here's the scenario that trips up most teams: you ship a new build, chunk filenames change (content-hash rotated), but some users are still loading the old index.html. That old HTML references 3.abc123.chunk.js โ which no longer exists because the new build produced 3.def456.chunk.js. Every navigation to a lazy route explodes.
Force no-cache on index.html at the server level
For Nginx:
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
location /static/ {
# Chunks are content-hashed โ cache them aggressively
add_header Cache-Control "public, max-age=31536000, immutable";
}
For Apache (.htaccess):
<Files "index.html">
Header set Cache-Control "no-cache, no-store, must-revalidate"
Header set Pragma "no-cache"
Header set Expires 0
</Files>
For AWS CloudFront, create a separate cache behavior for /index.html with TTL = 0.
With this in place, every deploy pushes a fresh index.html to users. Chunk references stay in sync. Problem solved at the source.
Step 3 โ Add an Error Boundary with Auto-Reload
Even with proper cache headers, some users will hit the stale-chunk window between deploy and CDN propagation. An error boundary catches the failure and reloads once โ silently fixing it for most people.
ChunkLoadErrors bubble up as uncaught promise rejections from dynamic imports. Wrapping lazy routes in a boundary lets you intercept them:
// ChunkErrorBoundary.tsx
import React, { Component, ErrorInfo } from 'react';
interface State {
hasError: boolean;
reloadAttempted: boolean;
}
export class ChunkErrorBoundary extends Component<React.PropsWithChildren, State> {
state: State = { hasError: false, reloadAttempted: false };
static getDerivedStateFromError(error: Error): Partial<State> {
if (error.name === 'ChunkLoadError') {
return { hasError: true };
}
return {};
}
componentDidCatch(error: Error, info: ErrorInfo) {
if (error.name === 'ChunkLoadError' && !this.state.reloadAttempted) {
this.setState({ reloadAttempted: true });
// Auto-reload once to pick up fresh chunks
window.location.reload();
}
}
render() {
if (this.state.hasError) {
return (
<div>
<p>Failed to load this section.</p>
<button onClick={() => window.location.reload()}>Reload</button>
</div>
);
}
return this.props.children;
}
}
Wrap your lazy routes:
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
function App() {
return (
<ChunkErrorBoundary>
<Suspense fallback={<Spinner />}>
<Dashboard />
</Suspense>
</ChunkErrorBoundary>
);
}
Step 4 โ Check PUBLIC_URL and CDN Base Path
Deployed to a subdirectory like /myapp/, or serving assets from a CDN? webpack needs to know the correct public path at build time, or the chunk URLs it generates will point nowhere.
In Create React App, set this in .env.production:
PUBLIC_URL=https://cdn.example.com/myapp
In a custom webpack config:
// webpack.config.js
module.exports = {
output: {
publicPath: process.env.PUBLIC_URL || '/',
// webpack 5 auto mode works well for most setups:
// publicPath: 'auto',
},
};
For Vite:
// vite.config.ts
export default defineConfig({
base: '/myapp/', // must match the deployment path exactly
});
Get this wrong by even one character and every chunk URL in the bundle will point to the wrong domain or path.
Step 5 โ Handle Chunk Load Failure at the Import Level
For critical lazy imports, add a retry wrapper. Mobile users on spotty 4G connections will thank you โ transient network blips cause chunk fetches to fail even when the file is right there on the server.
function retryImport(importFn: () => Promise<any>, retries = 3): Promise<any> {
return new Promise((resolve, reject) => {
importFn()
.then(resolve)
.catch((error) => {
if (retries <= 0) {
reject(error);
return;
}
setTimeout(() => {
retryImport(importFn, retries - 1).then(resolve, reject);
}, 1500);
});
});
}
// Usage
const Dashboard = React.lazy(() => retryImport(() => import('./pages/Dashboard')));
Three retries with a 1.5-second gap is usually enough to ride out a brief connectivity hiccup without frustrating the user.
Verify the Fix
- Deploy the new build. Open DevTools โ Network tab.
- Hard-reload (
Ctrl+Shift+R/Cmd+Shift+R). Confirmindex.htmlreturns200withCache-Control: no-cachein the response headers. - Navigate to a lazy-loaded route. Check that the chunk request returns
200and the filename matches what's in yourbuild/static/js/folder. - Simulate the stale-chunk scenario: deploy a fresh build, then manually request an old chunk URL โ it should 404. Reload the page โ the fresh
index.htmlshould load the correct new chunks cleanly. - Check the console. No
ChunkLoadError. Done.
Prevention
- Set
no-cacheonindex.htmlbefore your first production deploy โ not after the first incident. It takes five minutes and saves hours of on-call pain. - Purge CDN cache after every deploy. Add it to your pipeline:
aws cloudfront create-invalidation --distribution-id XXXXX --paths "/index.html". - Name your chunks to make debugging faster:
import(/* webpackChunkName: "dashboard" */ './pages/Dashboard')โ so the error saysdashboard.chunk.jsinstead of7.chunk.js. - Automate post-deploy verification. After deploying, have CI ping a few chunk URLs and confirm they return 200 before marking the deployment green.

