20

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 have changed the build path. Therefore, it is /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,
  },

Does someone know how to fix this issue?

Update:

I have never got this error in development. I use Sentry to track errors. It has happened at least 274 times in two months.

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 
}

Update

We had 500000 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 has happened on all kinds of browsers but mostly on Chrome.

I can not reproduce it either in dev nor in prod.

Update

I reproduced this bug finally. It happens if you are on a page and you release a new version. The file that contains the dynamically imported module, does not exist anymore, for eg:

https://help.example.io/static/js/IndexPage-fbd64584c.js

The above link returns 404.

mahan
  • 12,366
  • 5
  • 48
  • 83
  • 2
    I would look in your console, and see if there are more details. `Failed to fetch dynamically imported module:` is not a proper error,. – Keith May 25 '22 at 11:00
  • I know this isn't helpful, but the webpack's config code smells – Mechanic Aug 04 '22 at 09:33
  • @Mhmdrz_A What do you mean? I do not use webpack. – mahan Aug 04 '22 at 09:34
  • 1
    Are you able to reproduce locally ? or on the production site ? Do you have an idea of the frequency ? You can also expect some to fails for many reasons but since you are using sentry could you try to : isolate maybe a browser ? version ? the frequency ( ratio between number of load and number of fail ) – Ziyed Aug 04 '22 at 09:38
  • @Ziyed read the last Update. – mahan Aug 04 '22 at 09:52
  • 2
    Thanks for the update, with 0.3 this would mean like 30%, meaning out of 500k there is only ~274x3 = 1k visit that experience the issue, that about 0.2% visit if my math is correct. I don't think it would be worth the trouble for you but I can try to provide a long answer if you want. This error can be expected and can be triggered by many things, slow internet for example – Ziyed Aug 04 '22 at 09:59
  • Let me know if you want more of a detailed answer to that – Ziyed Aug 04 '22 at 10:00
  • Out of curiosity, can you also check timelines between those errors ? do they happen at particular time like releases ? for example ? – Ziyed Aug 04 '22 at 10:03
  • @Ziyad, No it happens like every day. Please only answer if you can fix this issue. – mahan Aug 04 '22 at 10:24
  • @mahan Having the same issue. Can't even reproduce. Are you using vite (v2.9.10)? – scarably Aug 04 '22 at 10:53
  • I am having the same issue with Svelte. After deploying to prod (Vercel) as usual now my main page content throws the error "Failed to fetch dynamically imported module...". It happens on my mobile but not on my PC, so definitely a client-side issue. No idea how to fix it. – SamHeyman Oct 18 '22 at 14:09
  • Your final update is what I also discovered. Do you know of a way to catch that error and just refresh the page? – Grant Nov 29 '22 at 09:38
  • 1
    @Grant, No. I do not know. – mahan Nov 29 '22 at 10:28
  • 1
    @Grant Check out my answer and the associated article. This is expected behavior and can be dealt with client side by wrapping the dynamic imports, but we really should get better browser support for this, for instance by explicit module cache invalidation. – oligofren May 09 '23 at 12:24

8 Answers8

6

Here are a few alternative takes on the issue.

Avoiding the issue altogether.

This is stating the obvious, and I suspect that this may not be a desirable answer from your perspective, but switching to static imports will fix your issue.

In many cases, the bundle size, even with static imports, will still be small enough not to affect your user experience, and definitely less so than encountering the error.

It is also arguably the only guaranteed way to solve it as even if your issue is caused by a malfunction somewhere that you manage to track down and fix, there are still legitimate situation where this issue can happen.

Managing the error elegantly

On the other hand, still in the mind-set that the error can and will still happen, React has the concept of ErrorBoundaries which you can leverage to deal with the user experience when it occurs : https://reactjs.org/docs/error-boundaries.html and https://reactjs.org/docs/code-splitting.html#error-boundaries

Taking matters into your own hands

Finally, the React lazy expects a function that returns a promise that resolves to a module. If it does not, well that's your TypeError. It is obviously intended to be used exactly like you did, but you are free to wrap that in something smarter if you wish. All you need to do is make sure that the promise eventually resolves to the imported module. For inspiration, see here for quite a few takes on retrying promises. Promise Retry Design Patterns

M. Gallant
  • 236
  • 2
  • 5
5

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:

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: reproduction with charles proxy.

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.

oligofren
  • 20,744
  • 16
  • 93
  • 180
4

It's hard to directly answers this question without all the data but I'll try to do my best and share a few ressources regarding that.

As discussed in the comments, there could be MANY reasons to why this happens. Since you can't reproduce, and it's not constant, we can probably exclude the theory where it's coming from your code or your deploy configs.

A few reasons on top of my head:

  • Network being slow at that moment when trying to fetch the resource. You could try to reproduce by throttling your network.
  • Parsing error or encountered an exception

I believe this error is similar to the 'ChunkLoadError' ( which is also faced in many other framework, not just React), attaching a similar question: Webpack code splitting: ChunkLoadError - Loading chunk X failed, but the chunk exists, note as well the same amount of traffic failing etc.

One attempt to fix this issue, try to catch the error and force a reload to refetch the resource, but make sure to not loop there.

I hope this helps.

Ziyed
  • 491
  • 2
  • 12
3

I think I might have found a good solution to the problem!

I was facing an identical situation. In my case, I use Vite in my React projects and every time the rollup generates the bundle chunks, it generates different hashes that are included in the filenames (Example: LoginPage.esm.16232.js). I also use a lot of code splitting in my routes. So every time I deployed to production, the chunk names would change, which would generate blank pages for clients every time they clicked on a link (which pointed to the old chunk) and which could only be resolved when the user refreshed the page (forcing the page to use the new chunks).

My solution was to create an ErrorBoundary wrapper for my React application that would "intercept" the error and display a nice error page explaining the problem and giving the option for the user to reload the page (or reload automatically)

Lucas Novaes
  • 103
  • 1
  • 8
  • Could you elaborate how that ErrorBoundary wrapper looks like? I have the same problem with a Vite + Vue 3 project. The error is indeed as you say based on failing to find a cached chunk file that is especially prone to change because we're developing a CMS where the user may add/remove/change pages and content items. Everytime the page is refreshed after a change, the error may popup and rarely it loads properly right away. – ShadowGames Jan 06 '23 at 12:50
0

I had the same issue when migrating from cra to vite and deploying it to netlify. There was no problem traversing through non-lazy components, but error occurs when trying to load the lazy ones even though it didn't occur when running it on my local computer (with vite & vite build).

If by any chance you're deploying to netlify, I had to turn off assets optimization and redeploy the application in order for the lazy components to load properly. If not, maybe a different set of settings similar to bundling or minify need to be turned off.

0

Fix for "Failed to fetch dynamically imported module"

Dynamically loaded modules are NOT the same as loading a typical JavaScript module file using a <script type='module' src='moduleloader.js' /> tag. This tag and attribute tells the browser the file is a module. If you dynamically load in module code into a non-module page without the script tag, it requires a few changes:

  1. Do NOT use the UNIX local relative path ./ format in non-module dynamic module import paths when linking to your module JavaScript file! Use normal HTML web paths like this instead, or it will FAIL: ../ or /

  2. Make sure you HTML page runs under JavaScript's 'use strict' mode! All ES6 Modules now run under strict mode by default, so make sure your non-module page runs that way!

  3. Make sure you test your import module calls on a real HTTP web server, not a local laptop or via the file system as the module will not load, otherwise.

EXAMPLE

Below is an example using a JavaScript Promise to call a module file into a non-module file. Both are simple web files. First create a HTML testpage.html file with this code in it:


<script type="text/javascript">
'use strict';
window.addEventListener('load', () => {
  import('/module1.js')// DO NOT USE "./"
    .then((m) => { alert(m.message.text); })// "Hello World!"
    .catch((e) => {alert('ERROR: '+e)})
});
</script>

Create a Module Export file called "module1.js" with this code to import:

const message = {
    text : "Hello World!"
};
export {message};

Be sure to save both files into the root of a web server and link to your http://yourdomain.com/testpage.html. An alert message should popup in your browser from your test page with a message imported in from the module JavaScript page.

Stokely
  • 12,444
  • 2
  • 35
  • 23
0

In my case it was the sass file I imported that contained an invalid variable and would not properly compile anymore. The error message is very unclear tho.

winkbrace
  • 2,682
  • 26
  • 19
0

In my case, I'm trying to do this

//MyImg.js file
import React from 'react'
    import ImgSrc from 'https://i.ytimg.com/vi/6xx6UYhJw-E/maxresdefault.jpg';
    
    function MyImg(props) {
      return (
        <div>
            <img src={ImgSrc} alt="no img" />
         
         </div>
      )
    }
    
    export default MyImg

so I got the below error

ERROR

Failed to fetch dynamically imported module: https://i.ytimg.com/vi/6xx6UYhJw-E/maxresdefault.jpg
TypeError: Failed to fetch dynamically imported module: https://i.ytimg.com/vi/6xx6UYhJw-E/maxresdefault.jpg

so Instead of declaring the image path above in that file, rather pass props like this via app.js file

//apps.js
 <MyImg src = "https://img1.hscicdn.com/image/upload/f_auto,t_ds_wide_w_1280,q_70/lsci/db/PICTURES/CMS/360000/360081.6.jpg"></MyImg>

and in MyImg file follows

//MyImg.js
import React from 'react'
function MyImg(props) {
  return (
    <div>
        <img src={props.src} alt="no img" />
     
     </div>
  )
}

export default MyImg

This above case is one of the reasons of getting the error. Thank you.