Failed to fetch dynamically imported module
I have a few React components that are lazy imported in App.tsx. App.tsx is used in Index.tsx where it is rendered and appended to the body.
const IndexPage = lazy(() => import("../features//IndexPage"));
const TagsPage = lazy(
() => import("../features/tags/TagsPage")
);
const ArticlePage = lazy(() => import("../features/article/ArticlePage"));
const SearchResultPage = lazy(
() => import("../features/search-result/SearchResultPage")
);
const ErrorPage = lazy(() => import("../features/error/ErrorPage"));
<BrowserRouter basename={basename}>
<Suspense fallback={<Fallback />}>
<Routes>
<Route path={INDEX} element={<IndexPage />} />
<Route path={ARTICLE} element={<ArticlePage />} />
<Route path={TAGS} element={<TagsPage />} />
<Route path={SEARCH} element={<SearchResultPage />} />
<Route path={ERROR} element={<ErrorPage />} />
<Route path="/*" element={<ErrorPage />} />
</Routes>
</Suspense>
</BrowserRouter>
Often, the following error happens in production:
Failed to fetch dynamically imported module:
It has happened in all routes:
https://help.example.io/static/js/SearchResultPage-c1900fe3.js
https://help.example.io/static/js/TagsPage-fb64584c.js
https://help.example.io/static/js/ArticlePage-ea64584c.js
https://help.example.io/static/js/IndexPage-fbd64584c.js
I changed the build path to /static/js.
build: {
assetsInlineLimit: 0,
minify: true,
rollupOptions: {
output: {
assetFileNames: (assetInfo) => {
var info = assetInfo.name.split(".");
var extType = info[info.length - 1];
if (/png|jpe?g|svg|gif|tiff|bmp|ico/i.test(extType)) {
extType = "img";
} else if (/woff|woff2/.test(extType)) {
extType = "css";
}
return `static/${extType}/[name]-[hash][extname]`;
},
chunkFileNames: "static/js/[name]-[hash].js",
entryFileNames: "static/js/[name]-[hash].js",
},
},
outDir: "./../backend/src/main/resources/static/articles/",
emptyOutDir: true,
},
How can I fix this? I never got this error in development. I use Sentry to track errors. This is all that is on Sentry:
{
arguments: [
{
message: Failed to fetch dynamically imported module: https://help.example.io/static/js/SearchResultPage-c1900fe3.js,
name: TypeError,
stack: TypeError: Failed to fetch dynamically imported module: https://help.example.io/static/js/SearchResultPage-c1900fe3.js
}
],
logger: console
}
I had 500,000 visits in the past two months. It happened 274 times. Since tracesSampleRate is 0.3 it is definitely more than that.
Sentry.init({
dsn: "",
integrations: [new BrowserTracing()],
tracesSampleRate: 0.3,
});
It happened on all kinds of browsers but mostly Chrome. It happens if you are on a page and release a new version. The file that contains the dynamically imported module does not exist anymore, for e.g.:
https://help.example.io/static/js/IndexPage-fbd64584c.js
It returns 404.Notice I wrapped up the below cache busting retry logic into an independent library of its own, so you can replace your import('./my-module.js') with import {dynamicImportWithRetry} from '@fatso83/retry-dynamic-import' ... const myModule = await dynamicImportWithRetry( () => import('./my-module.js')) This error is expected, not mysterious and by design (HTML spec): the HTML spec demands that failed dynamic imports are being cached. Here is the corresponding Chromium unit test that ensures it actually works like this. So what this means is that should a request fail for any number of reasons (many of which are listed above), your SPA is stuck between a rock and a hard place. This goes for any framework, library or other approach using dynamic imports. It is not React specific. It is unfortunately not always so easy to automatically solve for the users affected, as for some browsers it will sticky - both releading the page and restarting the browser will use the resolved network failure. Per May 2023 this happens in Chrome, but not so with Safari and Firefox where reloading will re-fetch failed dynamic imports as well (at least in my testing). Here's some light reading for background: Section of the spec https://tc39.es/ecma262/#sec-HostLoadImportedModule https://github.com/whatwg/html/issues/6768 https://github.com/tc39/proposal-dynamic-import/issues/80 https://bugs.chromium.org/p/chromium/issues/detail?id=1195405 You can reproduce and play with this using Charles Proxy by setting up TLS proxying and Rewriting the status code based on path. Here is one such example: . When verifying the fix I found using the Breakpoints feature of Charles Proxy very convenient: manually failing it on the first hit, then allowing it on subsequent requests. One of the other answers hint to using workaround related to React.lazy(). This the approach I currently went for: replacing React.lazy() calls with a wrapper function that does retry. Some approaches work better than others: a simple reload of the app (page) will not work (React LazyLoad gist for the reasons mentioned above cache busting will work An implementation of a simple cache-busting dynamic import wrapper can be found in this article which also a repo for playing with it. Nothing fancy (a more universal, cross-browser approach I came up with later is available in linked library). export const lazyWithRetries: typeof React.lazy = (importer) => { const retryImport = async () => { try { return await importer(); } catch (error: any) { // retry 5 times with 2 second delay and backoff factor of 2 (2, 4, 8, 16, 32 seconds) for (let i = 0; i < 5; i++) { await new Promise((resolve) => setTimeout(resolve, 1000 * 2 ** i)); // this assumes that the exception will contain this specific text with the url of the module // if not, the url will not be able to parse and we'll get an error on that // eg. "Failed to fetch dynamically imported module: https://example.com/assets/Home.tsx" const url = new URL( error.message .replace("Failed to fetch dynamically imported module: ", "") .trim() ); // add a timestamp to the url to force a reload the module (and not use the cached version - cache busting) url.searchParams.set("t", `${+new Date()}`); try { return await import(url.href); } catch (e) { console.log("retrying import"); } } throw error; } }; return React.lazy(retryImport); }; Note: The above approach matches on the exact error message produced by Chromium based browsers (Edge, Chrome, etc), so it does not work in Firefox, Safari, etc. In the library I linked to above I found another approach that should work across browsers to find the module name, but it makes some assumptions that might break in some environments.
Get this solution programmatically \u2014 free, no authentication.
curl https://depscope.dev/api/error/4ffc6beb27be0cc2120d71ea6184b72a6268d666592cd27dbbd06b0747348733