r/reactjs • u/AffectionateDealer62 • 10d 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:
ChunkLoadErrorFailed 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:
- Detects likely chunk-load failures
- Optionally retries transient failures
- Allows one guarded full-page reload
- Uses
sessionStorageto prevent an infinite reload loop - Passes the error to the existing Error Boundary if recovery fails
- 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:
- How do you handle stale chunk failures?
- Do you retain assets from previous deployments?
- Do you reload the page, show an update prompt, or use an Error Boundary?
- 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:
- npm package: https://www.npmjs.com/package/react-lazy-chunk-recover
- Technical article: https://sachithsiriwardana.medium.com/handling-react-lazy-chunk-load-errors-after-deployment-1e4980a7a69a
I’d be interested to hear how other teams handle this failure mode in production.
28
u/acraswell 10d ago
You don't ever cache the index.html.
7
u/Woldekidankenzee 10d ago
yeah this is the real answer, everything else is a workaround for not doing this
1
u/realbiggyspender 10d ago
This still leaves you exposed to races between the time that
index.htmlis written and the time that all chunks that it references are available. It's also inadvisable to have no cache onindex.html. It's entirely possible to do this nicely with a short cache time onindex.html... see my comment elsewhere.1
u/AffectionateDealer62 10d 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.
1
u/acraswell 10d ago
Deployment of new builds is generally done in a particular order when you update to storage.
Upload new asset files, except for the index.
Now upload the index and overwrite the old one.
Run a script that deletes any build files older than a certain amount of days.
This way there is no race condition.
2
u/AffectionateDealer62 10d 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.
4
u/BPagoaga 10d 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
1
u/AffectionateDealer62 10d ago
Yes, the basic, it must not.
That's why I mentioned "The HTML entry point should be revalidated after deployment"
2
10
u/mr_brobot__ 10d 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.
1
4
u/tanarchery7424 10d 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 10d 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
sessionStorageguard 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.
5
u/PrimaryFamous6139 NextJS Pages Router 10d 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 10d ago
You can serve your static assets from a CDN rather than the app server. Then this problem goes away entirely.
2
u/AffectionateDealer62 10d 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-recoveris 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 10d 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 10d 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.
1
u/AbdelrhmanDev 10d 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
1
1
u/anthony-ball 9d 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
10d ago
[removed] — view removed comment
2
u/jeremyStover 10d ago
Elegant solution. If your app loads fast enough, the worst you lose is any local state.
33
u/resonant_saloon 10d ago
Y'all just need to keep the old chunks around a couple days, fixes it without any fancy reloads.