CORE JSC

International Technology Partnership

Web Development & SEO

Fixing React.lazy Code-Split Chunks That Fail to Load After a New Deploy

A user has a tab open with the app loaded, a new version gets deployed in the background, and then they click a link to a route that was code-split with React.lazy. The app throws a blank white screen or a cryptic "Loading chunk 4 failed" error — not because anything about their click was wrong, but because the specific JavaScript file the old page is asking for no longer exists on the server.

Core JSC Team·October 4, 2026
Web DevelopmentReactCode SplittingDeploymentJavaScript

The Problem

An app uses React.lazy and dynamic import() to code-split routes or heavy components, each compiled into its own chunk file with a content hash in the filename (Settings.a1b2c3.js). A user has the app open in a tab from before a new deploy went out. They navigate to a route whose chunk wasn't already loaded, triggering a fetch for that specific hashed filename — which no longer exists on the server, because the new deploy replaced it with a differently-hashed file for the same route. The result is a thrown error (often surfacing as "Loading chunk X failed" or a generic blank screen) with no retry, no fallback, and no indication to the user of what actually happened or what to do about it.

Why It Happens

Content-hashed filenames are what make long-term caching safe, but they also mean old and new builds are never compatible

The whole point of hashing a chunk's filename by its content is that a CDN or browser can cache it indefinitely — the file never changes without its name also changing. But this means a page that loaded its initial HTML and main bundle before a deploy has no way to know the chunk filenames it's about to request were replaced; it still has the old filenames baked into its already-loaded JavaScript, and the server has no reason to keep serving files that no build step currently produces.

Dynamic import() rejects with no automatic retry when the requested file returns a 404

A failed dynamic import is just a rejected promise. Unless the application explicitly catches that rejection and does something about it — retrying, prompting a refresh, falling back to a full page navigation — the error propagates up (often to an uncaught promise rejection or an unhandled error in whatever triggered the lazy load) and the user is left looking at whatever broken or blank state that produces.

Old chunk files might be deleted from the server or CDN as part of the deploy process

Some deploy pipelines explicitly clean up old build artifacts to save storage, or a CDN's origin only serves the latest build's output directory. Either way, the previous deploy's chunk files can become genuinely unavailable (not just logically outdated) within moments of a new deploy completing, which is exactly when a user with an old tab open is most likely to trigger the failing request.

This specific failure is easy to miss in development because there's rarely a competing deploy mid-session

Local development almost never involves a chunk's hash changing out from under an already-running session, so this failure mode can go completely untested until it's reported by real users during or shortly after a production deploy — making it a class of bug that's specifically tied to the deploy lifecycle rather than to any particular code path being wrong.

The Fix

1. Catch dynamic import failures and retry once before giving up

function lazyWithRetry(importFn) {
  return React.lazy(() =>
    importFn().catch((error) => {
      // A single retry handles a transient network blip;
      // a genuinely stale chunk will fail again below
      return importFn();
    })
  );
}

const Settings = lazyWithRetry(() => import("./Settings"));

Wrapping the dynamic import with a retry handles transient network failures that have nothing to do with a stale deploy, cheaply ruling that out before concluding the failure is actually caused by a missing chunk file.

2. Detect a genuinely stale chunk and prompt a full page reload rather than showing a broken state

class ChunkErrorBoundary extends React.Component {
  state = { chunkFailed: false };

  static getDerivedStateFromError(error) {
    const isChunkError = /Loading chunk|Failed to fetch dynamically imported module/.test(
      error.message
    );
    return { chunkFailed: isChunkError };
  }

  render() {
    if (this.state.chunkFailed) {
      return (
        
A new version is available.
); } return this.props.children; } }

Catching this specific error pattern in an error boundary and offering a clear, explicit reload action turns an opaque crash into an understandable message — a full page reload fetches the current HTML and main bundle, which resolves to the new chunk filenames correctly, unlike a client-side retry of the same stale import.

3. Automatically reload once, rather than always requiring a manual click

function lazyWithReload(importFn) {
  return React.lazy(() =>
    importFn().catch((error) => {
      const alreadyReloaded = sessionStorage.getItem("chunk-reload-attempted");
      if (!alreadyReloaded) {
        sessionStorage.setItem("chunk-reload-attempted", "true");
        window.location.reload();
        return new Promise(() => {}); // reload is in flight; never resolve this import
      }
      throw error; // already tried reloading once — surface the error instead of looping
    })
  );
}

Automatically reloading once (guarded by a session flag to prevent an infinite reload loop if something else is genuinely broken) resolves the common case transparently, while still falling through to a visible error if a reload doesn't actually fix it — avoiding both a silent loop and an unnecessary manual step for the common, resolvable case.

4. Avoid deleting old build artifacts immediately, giving in-flight sessions a grace period

# Keep the previous N deploys' static assets available rather than
# replacing them in place — many static hosts and CDN configs support this
# e.g., versioned deploy directories, or a CDN cache that outlives the deploy

Where the deploy pipeline allows it, keeping recent previous builds' chunk files reachable for some grace period after a new deploy means a currently-open tab's stale chunk requests can still succeed normally, sidestepping the problem entirely for users who happen to navigate during that window rather than relying purely on client-side recovery.

Why This Works

Each fix addresses a different point in the failure chain from stale chunk to broken user experience. Retrying once rules out ordinary network flakiness before assuming a deploy-related cause; an error boundary that recognizes this specific error pattern turns an opaque crash into an actionable message; an automatic single reload resolves the common case without requiring the user to understand what happened; and keeping old build artifacts available for a grace period prevents the failure from occurring in the first place for sessions that overlap a deploy window.

Conclusion

A code-split chunk failing to load after a deploy isn't a bug in the lazy-loaded component itself — it's an old page asking for a file that a content-hashed build system, by design, no longer serves under that name. Retry the dynamic import once to rule out transient failures, catch the specific stale-chunk error pattern and prompt or trigger a reload rather than showing a blank screen, guard an automatic reload against looping if something else is actually broken, and where possible keep recent build artifacts available briefly so in-flight sessions aren't forced to hit this failure at all.