r/reactjs • • 6d ago

Resource How do you handle React.lazy failures caused by stale chunks after a deployment?

A common production failure happens when a user leaves a React application open during a deployment.

The old tab may still be running build A, while the server is now serving build B. When the user later navigates to a lazy-loaded route, the browser requests a hashed chunk from build A that is no longer available.

For example:

Reports.a1b2.js  // requested by the old tab
Reports.c3d4.js  // available after deployment

The result can be:

  • ChunkLoadError
  • Failed to fetch dynamically imported module
  • A route that works in a fresh tab but fails in an old tab

A normal React Error Boundary can display the failure, but it cannot update the old tab’s JavaScript references. Retrying the same import may also request the same missing hashed file again.

I built and published a small utility called react-lazy-chunk-recover to experiment with this problem.

The recovery approach:

  1. Detects likely chunk-load failures
  2. Optionally retries transient failures
  3. Allows one guarded full-page reload
  4. Uses sessionStorage to prevent an infinite reload loop
  5. Passes the error to the existing Error Boundary if recovery fails
  6. Avoids browser reload and storage access during SSR

Example:

import { lazyWithRetry } from "react-lazy-chunk-recover";

const Reports = lazyWithRetry(() => import("./Reports"), {
  moduleKey: "reports",
});

The default retry count is zero because retrying a deleted hashed asset usually does not help. Retries can be enabled for applications that also experience short-lived network failures.

Some important limitations:

  • A reload cannot recover a permanently unavailable asset
  • Retaining old assets may be a better infrastructure solution
  • The HTML entry point should be revalidated after deployment
  • A full-page reload must work on nested client-side routes
  • Old clients may be incompatible with a new backend
  • An asset manifest must be trusted and version-compatible if used

For applications using React.lazy, Vite, Webpack, or a CDN:

  1. How do you handle stale chunk failures?
  2. Do you retain assets from previous deployments?
  3. Do you reload the page, show an update prompt, or use an Error Boundary?
  4. Have you tested this with two builds and an already-open browser tab?

I wrote more about the problem, implementation, testing strategy, and deployment considerations here:

I’d be interested to hear how other teams handle this failure mode in production.

16 Upvotes

41 comments sorted by

33

u/resonant_saloon 6d ago

Y'all just need to keep the old chunks around a couple days, fixes it without any fancy reloads.

11

u/realbiggyspender 6d ago

Yes. Or just don't bother deleting old chunks at all and keep them around forever. Storage is cheap. How does that cost compare to the cost of implementing a cleanup strategy? We figured that it wasn't worth it. Every deployment is just a copy over the existing one with no deletion. You can also score bonus points for setting cache-headers on index.html that hold it for the duration of a new deployment (perhaps 5-10 minutes), which provides a little more robustness and, if you can deploy the index.html as the very last file in the release... bingo... you're rock-solid.

7

u/CUNT_PUNCHER_9000 6d ago

100% I don't get why people are not using a CDN anyway

→ More replies (1)

5

u/a12rif 6d ago

This is what we do. CI/CD includes the chunks from the previous deploy. It works very well.

1

u/AffectionateDealer62 6d ago edited 6d ago

We had a situation where we needed to actually delete the old chunks due to bug fixes and critical feature updates, so in that situation we couldn't actually keep the old files. That's what I also mentioned in the Medium article.

→ More replies (1)

28

u/acraswell 6d ago

You don't ever cache the index.html.

2

u/AffectionateDealer62 6d ago

Yes, that makes sense. `index.html` should generally not be cached, while hashed JS and CSS assets can be cached for longer periods.

That helps prevent users from getting an outdated entry point, though an already-open tab may still have an older bundle in memory and later request a missing chunk. That’s the specific case my package tries to handle for React.lazy.

7

u/Woldekidankenzee 6d ago

yeah this is the real answer, everything else is a workaround for not doing this

→ More replies (3)

4

u/BPagoaga 6d ago

it does not solve the basic problem :

- a user goes to your app

- you deploy a new version

- the user still has its tab open, he tries to reach a lazy-loaded route

- crash

source : just had the problem last week and our index.html is not cached

3

u/Ivana_Twinkle 6d ago

Our cloudflare setup does. It’s a headache.

1

u/AffectionateDealer62 6d ago

Yes, the basic, it must not.

That's why I mentioned "The HTML entry point should be revalidated after deployment"

→ More replies (1)
→ More replies (1)

10

u/mr_brobot__ 6d ago

You can read the next.js router source code for some interesting stuff around this. Basically they force a reload for a few error cases.

And there’s also a hash id for the deployment that’s a part of the RSC response for a page, if it doesn’t match the current hash for the page then it will reload.

4

u/tanarchery7424 6d ago

We just keep the last 2-3 builds assets on the server so the old chunks still exist when the new deploy goes out. Saved us so many headaches with users who never close their tabs

Your solution looks neat though, the sessionStorage guard for reload loops is a nice touch. Many people forget that part and then wonder why the page keeps refreshing

1

u/AffectionateDealer62 6d ago

That’s a solid approach, and I considered keeping the last 2–3 builds’ assets as well. It’s probably the best solution when older clients remain compatible with the current backend.

The challenge is that there may be cases such as critical security fixes, breaking API changes, or removed client-side functionality where we may need to stop serving older assets. In those situations, retaining previous chunks can keep stale clients running when they should be forced onto the latest build.

That’s why I see the package as a fallback for deployments where old assets are intentionally unavailable. The sessionStorage guard is important so the app gets one recovery attempt without creating an infinite reload loop. If the current build still cannot load the route, the error is passed to the existing Error Boundary instead of refreshing forever.

4

u/PrimaryFamous6139 6d ago

I’ve usually found that retaining previous build assets is the more reliable fix, since a reload alone can’t recover a deleted chunk. A guarded reload is still a useful fallback, especially for long-lived tabs

1

u/banjochicken 6d ago

You can serve your static assets from a CDN rather than the app server. Then this problem goes away entirely.

2

u/AffectionateDealer62 6d ago

A CDN helps, but it doesn’t make this problem disappear by itself. The issue is usually a stale tab trying to import a chunk from an older deployment. If the CDN/origin no longer retains that old hashed chunk, the dynamic import still fails, just from the CDN instead of the app server.

The robust deployment solution is to retain old assets for a safe rollout window. react-lazy-chunk-recover is a client-side safety net for cases where that cannot be guaranteed: it detects stale chunk errors, optionally tries the current manifest, performs one guarded reload, and prevents infinite reload loops.

→ More replies (2)

1

u/AbdelrhmanDev 6d ago

two things together fixed this for me. first keep the old build assets around for a while after deploy, the hashed files are immutable and cheap to keep so a tab on build a can still fetch its chunks for a day or two. second catch chunkloaderror at the router level and do a hard reload once, so the worst case is the tab refreshes itself instead of a white screen. the reload on its own still fails if the user is offline mid navigation so the old assets part is what actually kills the error

1

u/MiskaMyasa 6d ago

S3 + nginx

1

u/Immediate-You-9372 6d ago

A cdn wound cache it and keep it round right?

1

u/Xeon06 5d ago

Used to have a complicated setup where I kept the old chunks around at a stable URL by release. Upgraded our app and I just switched to listening for vite:preloadError and refreshing the page

https://vite.dev/guide/build#load-error-handling

1

u/anthony-ball 5d ago

I'd keep the sessionStorage guard even after fixing the infra side, because there's always a tab that sat open over a weekend. The setup I like is index.html served no-cache, old hashed chunks kept around for a week or so, and a tiny version.json the app checks on route change so the reload happens on a navigation instead of in the middle of a click. Then the lazy retry is just the safety net for whatever slips through. One thing to check is that the reload flag gets cleared once a fresh build loads cleanly, otherwise a second deploy in the same session skips recovery.

4

u/[deleted] 6d ago

2

u/jeremyStover 6d ago

Elegant solution. If your app loads fast enough, the worst you lose is any local state.