Introduction
When using code splitting with dynamic imports (import()), the browser downloads JavaScript chunks on demand. After a new deployment, old chunk files are deleted from the server, causing 404 errors for users who loaded the previous version and later navigate to a route that triggers a lazy load:
GET https://example.com/static/js/123.a1b2c3.chunk.js 404 (Not Found)
ChunkLoadError: Loading chunk 123 failed.
(error: https://example.com/static/js/123.a1b2c3.chunk.js)This breaks navigation and leaves users with a partially functional application.
Symptoms
- "ChunkLoadError" or "Loading chunk failed" error in the browser console
- Navigation to certain routes fails after a deployment
- 404 errors for JavaScript chunk files with old content hashes
- Application works after a full page refresh
- Error only affects users who had the app open during the deployment
Common Causes
- Deployment deletes old chunk files from the server/CDN
- User's SPA loaded the old version's HTML (which references old chunk hashes)
- Dynamic
import()resolves to a chunk that no longer exists on the server - Service Worker caches old chunk references
- Build tool generates new chunk hashes with every build, even without code changes
Step-by-Step Fix
- 1.Handle chunk load errors globally with an error boundary and retry:
- 2.```javascript
- 3.// React example with error boundary
- 4.class ChunkReloadErrorBoundary extends React.Component {
- 5.state = { hasError: false };
static getDerivedStateFromError(error) { if (error.message && error.message.includes('Loading chunk')) { return { hasError: true }; } return { hasError: false }; }
render() { if (this.state.hasError) { window.location.reload(); return null; } return this.props.children; } } ```
- 1.Catch chunk load errors in dynamic imports:
- 2.```javascript
- 3.const LazyComponent = React.lazy(() =>
- 4.import('./HeavyComponent').catch((error) => {
- 5.if (error.message.includes('Loading chunk')) {
- 6.// New version deployed, reload the page
- 7.window.location.reload();
- 8.return new Promise(() => {}); // Return a never-resolving promise
- 9.}
- 10.throw error;
- 11.})
- 12.);
- 13.
` - 14.Implement version detection to check for new deployments periodically:
- 15.```javascript
- 16.async function checkForNewVersion() {
- 17.try {
- 18.const response = await fetch('/version.json', { cache: 'no-store' });
- 19.const { hash } = await response.json();
- 20.const currentHash = window.__BUILD_HASH__;
if (hash && hash !== currentHash) { showUpdateNotification('A new version is available. Reload to update.'); } } catch (e) { // Silently fail version check } }
// Check every 5 minutes setInterval(checkForNewVersion, 5 * 60 * 1000); ```
- 1.Configure webpack chunk filename for debugging:
- 2.```javascript
- 3.module.exports = {
- 4.output: {
- 5.chunkFilename: '[name].[contenthash:8].chunk.js',
- 6.},
- 7.};
- 8.
` - 9.Set up a global webpack chunk load error handler:
- 10.```javascript
- 11.// In your entry point file
- 12.window.addEventListener('unhandledrejection', (event) => {
- 13.if (
- 14.event.reason &&
- 15.event.reason.message &&
- 16.event.reason.message.includes('Loading chunk')
- 17.) {
- 18.event.preventDefault();
- 19.window.location.reload();
- 20.}
- 21.});
- 22.
`
Prevention
- Always implement chunk load error handling in applications that use code splitting
- Generate a
version.jsonfile during build containing the git commit hash for deployment tracking - Keep old chunk files on the CDN for at least 24 hours after deployment to cover users with open tabs
- Use Service Worker strategies that cache the shell but use network-first for chunks
- Test deployment scenarios by loading the app, deploying a new version, then navigating to lazy-loaded routes
- Add chunk load error monitoring to your error tracking (Sentry, LogRocket) to detect deployment-related issues