Fix: Loading chunk failed / ChunkLoadError

The Error

You deploy your app and users start reporting blank pages or broken navigation. The console shows one of these:

Webpack (Create React App, Next.js Pages Router, custom config):

Uncaught ChunkLoadError: Loading chunk 42 failed.
(missing: https://example.com/static/js/42.abc123.chunk.js)
Loading chunk 42 failed.
(error: https://example.com/static/js/42.abc123.chunk.js)

Vite / ES module dynamic imports:

TypeError: Failed to fetch dynamically imported module: https://example.com/assets/Dashboard-abc123.js

React.lazy:

Uncaught ChunkLoadError: Loading chunk 42 failed.

Followed by:

The above error occurred in one of your React components. Consider adding an error boundary.

Next.js:

ChunkLoadError: Loading chunk 42 failed.
Error: Loading chunk pages/dashboard failed.

All of these mean the same thing: your app tried to load a JavaScript file on demand (a “chunk”) and the request failed. The file either doesn’t exist at that URL, returned the wrong content, or was blocked by the network.

Why This Happens

Modern bundlers split your app into multiple JavaScript files called chunks. Instead of loading everything upfront, your app fetches chunks on demand — when a user navigates to a route, opens a modal, or triggers a React.lazy component.

Each chunk filename includes a content hash (like 42.abc123.chunk.js). When you deploy new code, the hashes change. Here’s the problem:

  1. A user loads your app and gets the old index.html.
  2. That HTML references old chunk filenames.
  3. You deploy. The old chunk files are replaced by new ones with different hashes.
  4. The user navigates to a new page. The app requests 42.abc123.chunk.js.
  5. That file no longer exists on the server. The request returns a 404 or the new index.html (which is not JavaScript).
  6. The chunk fails to load.

This is the most common cause, but not the only one:

  • CDN or proxy caching serves stale index.html while origin has new chunks (or vice versa).
  • publicPath is wrong. Chunks are requested from the wrong URL entirely.
  • Service worker caches old HTML and serves it after deployment.
  • Network issues. Corporate proxies, ad blockers, or flaky connections block chunk requests.
  • Filename hashing is disabled. Without hashes, browsers cache old chunk content under the same filename.
  • Build output wasn’t fully uploaded. Partial deployments leave some chunks missing.

Fix

1. Handle the Stale Deployment Problem (Most Common)

The root cause is usually old HTML referencing chunks that no longer exist after a deployment. The fix has two parts: detect the failure and recover from it.

Force a page reload on chunk failure:

// For webpack (CRA, Next.js Pages Router, custom)
window.addEventListener('error', (event) => {
  if (
    event.message &&
    event.message.includes('Loading chunk') &&
    event.message.includes('failed')
  ) {
    // Avoid infinite reload loops
    const lastReload = sessionStorage.getItem('chunk-reload');
    const now = Date.now();

    if (!lastReload || now - parseInt(lastReload) > 10000) {
      sessionStorage.setItem('chunk-reload', now.toString());
      window.location.reload();
    }
  }
});

This works because reloading fetches the new index.html, which references the correct chunk filenames. The sessionStorage guard prevents infinite reload loops if the error is caused by something other than a stale deployment.

2. Retry Failed Dynamic Imports

Wrap your import() calls with a retry function. This handles transient network failures and gives the user’s browser another chance to fetch the chunk:

function retryImport(importFn, retries = 3, delay = 1000) {
  return new Promise((resolve, reject) => {
    importFn()
      .then(resolve)
      .catch((error) => {
        if (retries <= 0) {
          reject(error);
          return;
        }
        setTimeout(() => {
          retryImport(importFn, retries - 1, delay * 2).then(resolve, reject);
        }, delay);
      });
  });
}

// Usage
const Dashboard = React.lazy(() =>
  retryImport(() => import('./pages/Dashboard'))
);

The exponential backoff (delay * 2) avoids hammering the server. Three retries with 1s/2s/4s delays covers most transient failures.

For a reload-on-final-failure variant:

function importWithRetryAndReload(importFn, retries = 3) {
  return retryImport(importFn, retries).catch((error) => {
    const lastReload = sessionStorage.getItem('chunk-reload');
    const now = Date.now();

    if (!lastReload || now - parseInt(lastReload) > 10000) {
      sessionStorage.setItem('chunk-reload', now.toString());
      window.location.reload();
    }

    throw error;
  });
}

3. Add a React Error Boundary for Chunk Failures

React.lazy throws when a chunk fails to load. Without an error boundary, your entire app crashes to a white screen. Wrap lazy components in a boundary that catches chunk errors specifically:

import React from 'react';

class ChunkErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasChunkError: false };
  }

  static getDerivedStateFromError(error) {
    if (error.name === 'ChunkLoadError' ||
        error.message?.includes('Loading chunk') ||
        error.message?.includes('dynamically imported module')) {
      return { hasChunkError: true };
    }
    // Let other errors propagate
    throw error;
  }

  render() {
    if (this.state.hasChunkError) {
      return (
        <div>
          <p>A new version is available.</p>
          <button onClick={() => window.location.reload()}>
            Reload page
          </button>
        </div>
      );
    }
    return this.props.children;
  }
}

// Usage
<ChunkErrorBoundary>
  <React.Suspense fallback={<div>Loading...</div>}>
    <Dashboard />
  </React.Suspense>
</ChunkErrorBoundary>

This is better than a silent reload because it gives the user context. They see “A new version is available” instead of a mysterious page refresh.

4. Fix CDN and Caching Issues

If your CDN caches index.html, users get stale HTML that references old chunks even after deployment.

Set correct cache headers for index.html:

Cache-Control: no-cache, no-store, must-revalidate

Set long cache headers for hashed assets:

Cache-Control: public, max-age=31536000, immutable

The hashed filenames (42.abc123.chunk.js) are unique per build, so they can be cached forever. The HTML must never be cached because it’s the entry point that references specific chunk filenames.

For Nginx:

location / {
  # HTML files - never cache
  if ($request_filename ~* \.html$) {
    add_header Cache-Control "no-cache, no-store, must-revalidate";
  }
}

location /static/ {
  # Hashed assets - cache forever
  add_header Cache-Control "public, max-age=31536000, immutable";
}

For Vercel/Netlify: These platforms handle this correctly by default for Next.js and most frameworks. If you’re seeing chunk errors on these platforms, the issue is likely elsewhere.

For CloudFront: Create a cache behavior that uses a managed cache policy with CachingDisabled for index.html, and CachingOptimized for /static/* or /_next/*.

5. Fix publicPath Misconfiguration

If chunks are requested from the wrong URL entirely, publicPath is wrong.

Webpack (webpack.config.js):

module.exports = {
  output: {
    publicPath: '/', // Must match where your files are actually served
  },
};

Common mistake: setting publicPath: '' or a relative path when your app is served from a subdirectory. If your app lives at https://example.com/app/, set:

output: {
  publicPath: '/app/',
}

Create React App: Set the homepage field in package.json:

{
  "homepage": "/app/"
}

Vite (vite.config.js):

export default defineConfig({
  base: '/app/',
});

Open the browser DevTools Network tab when the error occurs. Look at the URL of the failing request. If it points to the wrong path, publicPath or base is the problem.

6. Clear the Service Worker Cache

If your app uses a service worker (common with Create React App’s serviceWorker.register() or a PWA setup), the service worker may cache old HTML and serve it even after deployment.

Force the service worker to update:

// In your service worker registration
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js').then((registration) => {
    registration.addEventListener('updatefound', () => {
      const newWorker = registration.installing;
      newWorker.addEventListener('statechange', () => {
        if (newWorker.state === 'activated') {
          // New service worker active, reload to get new assets
          window.location.reload();
        }
      });
    });
  });
}

Nuclear option — unregister the service worker entirely:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.getRegistrations().then((registrations) => {
    for (const registration of registrations) {
      registration.unregister();
    }
  });
}

To manually clear for yourself: Open DevTools → Application → Service Workers → Unregister. Then Application → Cache Storage → delete all caches. Reload.

7. Keep Old Chunks During Deployment (Immutable Deployments)

The cleanest fix for the stale deployment problem is to not delete old chunks when you deploy. Keep at least two versions of chunks live at the same time.

How to do this:

  • Deploy to a new directory each time (e.g., /_next/static/BUILD_ID/) and keep the previous build’s directory. Next.js does this automatically.
  • On S3/CloudFront, upload new files without deleting old ones. Set a lifecycle rule to clean up files older than 24 hours.
  • On a traditional server, deploy to a new directory and symlink:
# Deploy
cp -r build/ /var/www/releases/$(date +%s)/
ln -sfn /var/www/releases/$(date +%s) /var/www/current

# Clean up old releases (keep last 3)
ls -dt /var/www/releases/*/ | tail -n +4 | xargs rm -rf

This eliminates the window where old HTML references deleted chunks.

8. Next.js-Specific Solutions

Next.js has its own chunk loading behavior that requires specific fixes. If you’re also seeing hydration errors alongside chunk failures, see Fix: Hydration failed because the initial UI does not match.

App Router — use loading.js for built-in Suspense boundaries:

Next.js App Router wraps each route in a Suspense boundary if you provide a loading.js file. This catches chunk load failures at the route level:

app/
  dashboard/
    page.js
    loading.js    ← Acts as Suspense fallback
    error.js      ← Catches chunk errors

Create an error.js that handles chunk failures:

'use client';

import { useEffect } from 'react';

export default function Error({ error, reset }) {
  useEffect(() => {
    if (
      error.message?.includes('Loading chunk') ||
      error.name === 'ChunkLoadError'
    ) {
      // Chunk load failure — reload to get new deployment
      window.location.reload();
    }
  }, [error]);

  return (
    <div>
      <h2>Something went wrong</h2>
      <button onClick={() => reset()}>Try again</button>
    </div>
  );
}

Pages Router — use next/dynamic with error handling:

import dynamic from 'next/dynamic';

const Dashboard = dynamic(() => import('../components/Dashboard'), {
  loading: () => <p>Loading...</p>,
  ssr: false,
});

next/dynamic is Next.js’s wrapper around React.lazy. It supports SSR control and a loading state out of the box.

Check your assetPrefix and basePath: If you serve Next.js assets from a CDN, make sure assetPrefix in next.config.js points to the right URL:

// next.config.js
module.exports = {
  assetPrefix: 'https://cdn.example.com',
  // OR for subdirectory deployments:
  basePath: '/app',
};

If the chunk request URLs in DevTools don’t match where your files actually live, this is the problem. See module resolution issues for more on path misconfiguration.

9. Fix Ad Blockers and Browser Extensions Blocking Chunks

Some ad blockers and privacy extensions block JavaScript files that match certain patterns. If a chunk filename happens to contain strings like ad, analytics, track, or banner, it may be blocked.

How to confirm: Check the Network tab. If the chunk request shows ERR_BLOCKED_BY_CLIENT, an extension is blocking it.

Fix: Rename the chunk. In webpack, use chunkFilename with a custom naming pattern:

// webpack.config.js
output: {
  chunkFilename: 'static/js/[contenthash:8].js',
}

Or use webpack’s magic comments to name chunks explicitly:

const Dashboard = React.lazy(() =>
  import(/* webpackChunkName: "dash-view" */ './pages/Dashboard')
);

Avoid chunk names that contain words commonly blocked by ad filter lists.

Still Not Working?

Check for partial deployments. If your CI/CD pipeline uploads files to S3 or a CDN, the upload might not be atomic. HTML may reference chunks that haven’t been uploaded yet. Fix this by uploading all assets first, then updating index.html (or the equivalent entry point) last.

Check for CORS issues on your CDN. If chunks are served from a different domain than your HTML (e.g., cdn.example.com vs app.example.com), you need CORS headers on the CDN:

Access-Control-Allow-Origin: https://app.example.com

Without this, the browser blocks the chunk request silently. See Fix: CORS Access-Control-Allow-Origin for details on configuring cross-origin headers.

Check for integrity hash mismatches. If you use Subresource Integrity (SRI), a CDN that modifies files (minification, compression changes) will cause integrity checks to fail. Either disable SRI or ensure your CDN serves files byte-for-byte identical to what you built.

Check your reverse proxy or load balancer. If you have multiple servers and only some received the new deployment, requests may hit a server that doesn’t have the new chunks. Ensure all servers are updated before routing traffic.

Your Vite build uses relative paths. If base is set to './' in vite.config.js, dynamic imports may resolve relative to the current page URL rather than the app root. This breaks when navigating to nested routes. Set base: '/' unless you specifically need relative paths.

webpack_require is loading chunks from the wrong origin. If you use micro-frontends or Module Federation, each remote needs its own publicPath configured correctly. A mismatch means one app requests chunks from another app’s origin, which doesn’t have them.

Your build isn’t generating content hashes. If your webpack config uses [name].js instead of [name].[contenthash].js for output filenames, browsers cache old chunk content under the same filename. New deployments serve new content, but the browser uses its cached version — which may reference imports that no longer exist in the new build. Always use content hashes:

output: {
  filename: '[name].[contenthash:8].js',
  chunkFilename: '[name].[contenthash:8].chunk.js',
}

React Router or Next.js prefetching fails silently. Both frameworks prefetch chunks for linked routes. If prefetching fails (user went offline, VPN dropped), the chunk isn’t cached, and navigation fails later. Combine prefetch with the retry strategy from Fix 2 to handle this.

If you’re seeing import resolution errors at build time rather than chunk loading errors at runtime, the problem is different — your bundler can’t find the module during compilation, not during dynamic loading in the browser.

Related Articles

Fix: Module not found: Can't resolve / Cannot find module or its corresponding type declarations

How to fix 'Module not found: Can't resolve' in webpack, Vite, and React, and 'Cannot find module or its corresponding type declarations' in TypeScript. Covers missing packages, wrong import paths, case sensitivity, path aliases, node_modules corruption, monorepo hoisting, barrel files, and asset imports.

Fix: Hydration failed because the initial UI does not match what was rendered on the server (Next.js)

How to fix the Next.js hydration mismatch error. Covers invalid HTML nesting, browser extensions, Date/time differences, useEffect for client-only code, dynamic imports, suppressHydrationWarning, localStorage, third-party scripts, Math.random, auth state, and React portals.

Fix: [vite] Internal server error: Failed to resolve import

How to fix Vite's 'Failed to resolve import' error, including 'Does the file exist?', 'Optimized dependency needs to be force included', 'Pre-transform error', and '504 (Outdated Optimize Dep)'. Covers missing packages, path aliases, optimizeDeps, cache clearing, and CJS/monorepo edge cases.

Fix: Hydration failed because the initial UI does not match what was rendered on the server

How to fix Next.js and React hydration errors including 'Text content does not match server-rendered HTML', 'Expected server HTML to contain a matching <div> in <div>', and 'There was an error while hydrating'. Covers browser extensions, Date/time mismatches, invalid HTML nesting, window checks, localStorage, and third-party components.