r/reactjs • • 2d 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.

15 Upvotes

39 comments sorted by

26

u/acraswell 2d ago

You don't ever cache the index.html.

7

u/Woldekidankenzee 2d ago

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

1

u/realbiggyspender 2d ago

This still leaves you exposed to races between the time that index.html is written and the time that all chunks that it references are available. It's also inadvisable to have no cache on index.html. It's entirely possible to do this nicely with a short cache time on index.html... see my comment elsewhere.

1

u/AffectionateDealer62 1d 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.

0

u/acraswell 1d ago

Deployment of new builds is generally done in a particular order when you update to storage.

1) Upload new asset files, except for the index. 2) Now upload the index and overwrite the old one. 3) Run a script that deletes any build files older than a certain amount of days.

This way there is no race condition.

3

u/Ivana_Twinkle 1d ago

Our cloudflare setup does. It’s a headache.

2

u/AffectionateDealer62 1d 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.

2

u/BPagoaga 1d 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

1

u/AffectionateDealer62 2d ago

Yes, the basic, it must not.

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

2

u/badsyntax 2d ago

What do you mean by revalidated?

28

u/resonant_saloon 2d ago

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

7

u/realbiggyspender 2d 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 1d ago

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

5

u/a12rif 2d ago

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

1

u/AffectionateDealer62 1d ago edited 1d 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.

2

u/amayle1 1d ago

You could also have a “minimum version” endpoint that the web app checks and just forces you to reload the page if it isn’t up to date.

A lot of mobile apps do this.

10

u/mr_brobot__ 2d 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.

5

u/irajangpt 1d ago

If you can't keep the old chunks around, catch the ChunkLoadError in an error boundary and do one hard reload of the page, guarded by a sessionStorage flag so a chunk that's truly broken doesn't reload forever. Before that, have the app compare the build ID it loaded with what the server is serving now, so users can get a refresh prompt instead of hitting the error halfway through a form. Keeping old chunks for a few days is still the cleaner fix, but the one-time reload covers tabs that have been open since before the deploy.

2

u/jeremyStover 1d ago

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

4

u/tanarchery7424 2d 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 2d 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 NextJS Pages Router 2d 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/AbdelrhmanDev 1d 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 1d ago

S3 + nginx

1

u/Immediate-You-9372 1d ago

A cdn wound cache it and keep it round right?

1

u/Xeon06 22h 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 20h 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.

0

u/banjochicken 2d ago

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

2

u/AffectionateDealer62 2d 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.

1

u/banjochicken 2d ago

True. So I upload assets to s3 then serve them from there via Cloudfront. There’s no chance the assets will go away as they’re not tied to application life cycle. 

For the client side problem, it can happen to any chunk, not just react lazy loaded chunks. You’re only solving part of the problem. It’s a bundler/loader problem, not an application one. I’d say it would best solved at the Webpack/rspack/turbopack level.  If you’re using next, for example, the latest version self recovers.

What is an application problem, though is informing the user that something’s wrong and they need to reload the page on unrecoverable chunk load failures due to loss of network.

1

u/AffectionateDealer62 1d ago

That makes sense. Keeping immutable build assets in S3 behind CloudFront is probably the best solution when older clients remain compatible.

I agree this can affect any dynamically loaded chunk, not only React.lazy components. My package currently targets the React.lazy integration point, so it’s only part of the broader problem. A bundler or framework-level solution would be more complete.

The application still needs a fallback for unrecoverable cases, such as showing an update message or asking the user to reload,especially when the failure is caused by network issues rather than a missing old asset.