loading...

How to retry when React lazy fails

goenning profile image Guilherme Oenning Originally published at goenning.net 惻Updated on 惻2 min read

React 16.6 has been released and it's now easier than ever to do code split within our React applications by using lazy and Suspense.

If you don't know what I'm talking about, you should definitely read this https://reactjs.org/blog/2018/10/23/react-v-16-6.html

After a few days monitoring a production application that is using lazy, I noticed some client-side errors like this:

Loading chunk 6 failed. (error: https://.../6.4e464a072cc0e5e27a07.js)
Loading CSS chunk 6 failed. (https://.../6.38a8cd5e9daba617fb66.css)    

Why?!

I don't actually know why, but I can only assume this is related to the user network. Maybe they are on a slow 3G and there was a network hiccup? That's not a rare event, right?

Alright, if that's true, how do we solve that?

We can do the same thing that everyone does when a network request fails: retry it! šŸ˜„

How?

Did you know that the import(...) function that we use on lazy is just a function that returns a Promise? Which basically means that you can chain it just like any other Promise.

Below you can find a basic implementation of a retry function.

function retry(fn, retriesLeft = 5, interval = 1000) {
  return new Promise((resolve, reject) => {
    fn()
      .then(resolve)
      .catch((error) => {
        setTimeout(() => {
          if (retriesLeft === 1) {
            // reject('maximum retries exceeded');
            reject(error);
            return;
          }

          // Passing on "reject" is the important part
          retry(fn, retriesLeft - 1, interval).then(resolve, reject);
        }, interval);
      });
  });
}

Source: https://gist.github.com/briancavalier/842626

Now we just need to apply it to our lazy import.

// Code split without retry login
const ProductList = lazy(() => import("./path/to/productlist"));

// Code split with retry login
const ProductList = lazy(() => retry(() => import("./path/to/productlist")));

If the browser fails to download the module, it'll try again 5 times with a 1 second delay between each attempt. If even after 5 tries it import it, then an error is thrown.

That's all! šŸŽ‰

Thanks!

Discussion

pic
Editor guide
Collapse
mattdevio profile image
Matt G

Is this good practice? It seems like just showing a toast or broken skeleton would be better. It could be a LONG time before a user sees an error message. I think letting the user attempt a reload is a better experience. I could be wrong. I honestly dont know.

Collapse
goenning profile image
Guilherme Oenning Author

Maybe not, but that'll depend a lot from one app to another. This is just one solution to the problem.

We actually mix both. We retry 5 times with a 500ms interval, if it still fails, then the ErrorBoundary catches the error and show a generic error page. I think that 2.5 sec is not that long if we can avoid having the user seeing an unnecessary error page.

Collapse
mattdevio profile image
Matt G

My concern was getting 5 15sec timeout requests without any relevant feedback as to what happened. Still useful information. Thanks :)

Collapse
kenshinman profile image
Kehinde Orilogbon

Honestly, this works for me. I have an app that is being used in a place with very slow network. This has helped me solve my client's problem. thanks @goenning

Collapse
maininfection profile image
Ricardo Machado

Hi Guilherme,

The reason for failing to download might be related to a recent deployment which changes the chunk hash and/or order number.

Also, don't forget to return the retry on the catch. Otherwise whoever gets the first retry cannot get the rejected error.

nice helper šŸ‘

Collapse
debo07 profile image
Debajit Majumder

Could you please elaborate on this - "The reason for failing to download might be related to a recent deployment which changes the chunk hash and/or order number."?

I believe I am facing similar issue where just after a new deployment I able to see blank page with this console error.

Uncaught (in promise) Error: Loading chunk 7 failed.
(missing: domain.com/js/vendors~module1.bund...)
at HTMLScriptElement.i (bootstrap:120)

Collapse
rainydaydy profile image
č‘£é›Ø

when use contenthash, if your file don't change, but add a new route, the chunkId will change,so the bundle is change but the filename doesn't change, when load chunk 1, the resource's chunkId maybe 2, so load chunk failed

Collapse
goenning profile image
Guilherme Oenning Author

I don't think you need to do that. If the second retries fails, you get a new error, which is likely to be the same as the first retry error. But maybe I misunderstood your argument.

Collapse
maininfection profile image
Ricardo Machado

Ahhh I mislooked the last retry line. It actually passes the resolve and reject from the initial promise.

šŸ‘

Collapse
azrizhaziq profile image
Azriz Jasni

Nice helpers, there is a simple mistake on your code

  // from 
  retry(fn, interval, retriesLeft - 1).then(resolve, reject);

  // to
  retry(fn, retriesLeft - 1, interval).then(resolve, reject);

šŸ˜†

Collapse
goenning profile image
Guilherme Oenning Author

Nice catch! Thank you šŸ™

Collapse
aditya_2911 profile image
M ADITYA

Thumbs up for this post. But I have a question as to when you are loading this new chunk...
for example: after a new deployment on the server, you definitely would fetch the updated chunk with the retries, but what about the initial bundle thats already been fetched (that stale bundle wouldn't be updated with the new bundle deployed right?). This could potentially lead to problems such as the newly fetched chunk being incompatible with the old bundle, etc.

Collapse
eschiebel profile image
Ed Schiebel

if the if (retriesLeft === 1) { block was moved above the setTimeout, the user wouldn't have to wait another interval for the final rejection.

Collapse
pranavtheway profile image
Pranav Dave

Thank You so much for bringing this... but this solution perfectly fine in Chrome but it doesnt work in IE11, The retry doesnt happen at all... what could be the reason behind this behaviour!!??

Collapse
snesjhon profile image
Jhon Paredes

Hi Guilherme, Iā€™m wondering how you ended up testing this implementation.

Did you slow down your network using dev tools or manually forced a network fail?

Collapse
goenning profile image
Guilherme Oenning Author

Yes, I forced it to be offline (using Chrome). Wait for like 2 seconds and put it back online šŸ˜€

Collapse
carpben profile image
Ben Carp

Checking your logs, can you say if this solution reduced the number of loading errors and by how much (in percentage)?

Collapse
hugoliconv profile image
Hugo

I'm getting these errors too but I'm not sure how they are shown to the user. Do you have an idea?

Collapse
prateek3255 profile image
Prateek Surana

Awesome helper function.

Can you please also tell me how can I use it with react-loadable, because the loader doesn't expect a promise actually, it expects the import function?