How to overcome loading chunk failed with Angular lazy loaded modules
Asked Answered
D

13

76

If I make changes to my angular app the chunk names will change on build and the old version will be removed from the dist folder. Once deployed, if a user is currently on the site, and then navigates to another part of the site, I get a "loading chunk failed" error because the old file is no longer there.

My app is built using Angular CLI and is packaged using webpack.

Is there any way this can be fixed?

Dolerite answered 9/3, 2018 at 16:37 Comment(8)
Angular now supports service workers so you can use those to notify the user that a new version of the app is available to download.Yesterday
I have thought of that but my concern with that method is it's something people are not very familiar with on the web and they might find it a bit strange. My site is an ecommerce site and I don't want to be putting them off.Dolerite
I agree that for such a public type of app you don't want to do that. I'm not sure what is a good solution then, sorry.Yesterday
@Dolerite did you mange to solve this? I Have the exect same issue with react,Tribe
@Tribe Unfortunately not, I even tried using cloudfront to cache the files for a couple of weeks in the hope it would make them available after an update, but this is clearly not the issue as I'm still getting the error on the odd occasion and not been able to determine why.Dolerite
Do you find the solution for this issue?Munday
This is one solution to the problem I found. medium.com/@kamrankhatti/…Dolerite
Adding to @rmcsharry's comment, yes service workers solve the issue pretty elegantly. Service worker allows you to identify that the new app bundle was deployed. Then you have multiple options. Some are, 1) notify user regarding a new version and provide a Refresh button; 2) force app refresh on a nearest successful navigation event, in this case you're sure user don't loose any form data (of course, it depends on your app implementation) and no user action is neededBowleg
T
55

UPDATED SOLUTION BELOW Preferred solution is converting to PWA


I had the same problem and found a pretty neat solution that is not mentioned in the other answers.

You use global error handling and force app to reload if chunks failed.

import {ErrorHandler, Injectable} from '@angular/core';

@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
  
  handleError(error: any): void {
   const chunkFailedMessage = /Loading chunk [\d]+ failed/;

    if (chunkFailedMessage.test(error.message)) {
      window.location.reload();
    }
    console.error(error);
  }
}

It is a simple solution and here (Angular Lazy Routes & loading chunk failed) is the article I got it from.


EDIT: Convert to PWA

Another solution can be to convert your Angular site to a PWA (Progressive Web App). What this does is add a service worker that can cache all your chunks. Every time the website is updated a new manifest file is loaded from the backend, if there are newer files, the service worker will fetch all the new chunks and notify the user of the update.

Here is a tutorial on how to convert your current Angular website to a PWA. Notifying the user of new updates is as simple as:

import { Injectable } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';

@Injectable()
export class PwaService {
  constructor(private swUpdate: SwUpdate) {
    swUpdate.available.subscribe(event => {
      if (askUserToUpdate()) {
        window.location.reload();
      }
    });
  }
}
Tanberg answered 27/11, 2019 at 15:2 Comment(13)
Neat idea to catch the error! It seems this would risk causing a reload loop if the chunk load failure persists?Firedamp
@MychalHackman I don't think it is a problem. If you are trying to navigate to a chunk that is not present on the server, the reload will land you on the last successful loaded chunk. I have tested this myself by renaming chunks and forcing the error.Tanberg
this answer is incomplete, reloading the page won't force-refresh the browser cache so this will only work for some browsers. You should clear the cache first using something like await Promise.all((await caches.keys()).map(caches.delete))Heterogenous
@Heterogenous It is not necessary to refresh the cache because of hashing of the chunks. When you reload there will be a new main.XXXX.js with reference to all the new hashed bundles.Tanberg
This happened to me but was due to a timeout. In that case couldn't this cause an infinite loop unless you prompt the user to reload?Kanarese
Infinite-reload-loop may also happen e.g if mixpanel.chunk.* cannot be loaded because of AdBlockerVivianviviana
You could always add a reload=true boolean to the requested URL, (or reload=timestamp) making the URL unique for the browser, and then if it's in the URL do not do another reload.Pigpen
I don't recommend this. reasons: 1. There might be a problem in loading a chunk. so it'll result in infinite refreshing. 2. Cache issues. So I recommend displaying an alert asking the user to refresh the pageWolgast
@Wolgast I updated my question to add the PWA solution which will avoid any issues.Tanberg
I modified the regexp to /Loading chunk [^\s]+ failed/ because the name of my chunk was not just numbers.Bailable
@TetsutoYabuki thanks! this worked, it seems the error message changed at some point in time and the accepted answer's regexp is no longer workingFluviomarine
@Fluviomarine I would suggest using the PWA solution instead.Tanberg
How to fix this chunk load issue for chunks that still aren't loading even after refreshing the page???Swanky
C
11

You can use custom GlobalErrorHandler in your application, All it does is just check the error message if it has a Loading chunk [XX] failed, and forces the app to reload and load the new fresh chunks.

Create globalErrorHandler with the following code

import { ErrorHandler, Injectable } from "@angular/core";
@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
    handleError(error: any): void {
        const chunkFailedMessage = /Loading chunk [\d]+ failed/;
        if (chunkFailedMessage.test(error.message)) {
            if (confirm("New version available. Load New Version?")) {
                window.location.reload();
            }
        }
    }
}

After creating globalErrorHandler.ts, you should define the ErrorHandler in app.module.ts

@NgModule({
  providers: [{provide: ErrorHandler, useClass: GlobalErrorHandler}]
})
Corson answered 11/3, 2021 at 11:52 Comment(2)
The code works fine, but facing issues while writing unit tests for the service added. if window.onbeforeunload = jasmine.createSpy(); is used in the beforeeach block, the unit tests run in loop and are never finished. If window.onbeforeunload = jasmine.createSpy(); is not used then the tests fail mentioning "Some of your tests load the entire page". Can someone please help?Addlepated
Just remember to add console.error(error); after the first if otherwise you lose ALL error messages in the console.Tanberg
O
7

For applications that use lazy loading, these errors typically happen either during the initial load process or during a lazy-loaded route navigation.

During initial load it can be difficult to determine the state of the application and handle the error appropriately. I'm reluctant to introduce the possibility of an infinite reload cycle in this case, so I do not attempt to automatically reload the app, just show an error message telling the user that they need to reload the page to continue.

During navigation, chunk load errors can be handled like this:

router.events
    .pipe(
        filter(evt => evt instanceof NavigationError),
        map(evt => evt as NavigationError)
    )
    .subscribe(evt => {
        if (evt.error instanceof Error && evt.error.name == 'ChunkLoadError') {
            window.location.assign(evt.url);
        }
    });

This has the advantage of seamlessly loading the desired route.

Orelle answered 1/11, 2023 at 12:36 Comment(2)
If you're using angular 18+ or lazy loading routes/components etc. This is the way.Ligialignaloes
@JamesClaridge Good point: I should clarify that this is primarily useful for apps that use lazy loading.Orelle
C
6

If the Angular application is in an unrecoverable state, the Angular docs suggestion is to subscribe to the Service Worker's unrecoverable updates/events then handle by what makes sense to your app.

e.g. reload the page, or notify the user, etc:

@Injectable()
export class HandleUnrecoverableStateService {
  constructor(updates: SwUpdate) {
    updates.unrecoverable.subscribe(event => {
      notifyUser(
        'An error occurred that we cannot recover from:\n' +
        event.reason +
        '\n\nPlease reload the page.'
      );
    });
  }
}

i.e. the current version of the user's app has a reference to a file not in the cache and not accessible from the web server, which is typically what happens in this Chunk Load error scenario.

Constant answered 27/6, 2022 at 8:30 Comment(0)
L
5

I keep my old chunks in place for a couple days after an update for just this purpose. My app also consists of mini-SPAs, so as they move around, they're likely to pick up the new version during a page load.

Labarum answered 21/6, 2018 at 21:31 Comment(1)
This is not ideal if you need to fix something urgently in a chunk.Tanberg
V
2

I know I'm a little late to the game on this questions, but I would recommend changing your deployment approach.

Check out https://immutablewebapps.org/. The basic philosophy is to isolate you build time assets from your runtime variables. This provides a bunch of benefits, but the biggest are:

  • No caching problems for assets because they are isolated at the route level
  • The same code that was vetted in development is used in production
  • Instant fallback with hot cache for consumers in the case of a problem
  • Chunks that are created are maintained for previous version isolated by routes avoiding the active deployment vs active user problem

This also should not interfere with the ** PreloadAllModules** suggestion offered by @dottodot below.

Vulgarize answered 5/1, 2019 at 12:20 Comment(2)
Can you elaborate on "isolate you build time assets from your runtime variables", I don't understand how to start applying this on my app.Claritaclarity
Seems that the .org linked in this answer at time of writing is now a link farm. possibly try immutablewebapps.com instead?Ornithic
G
1

You can also use service workers to detect the change with "polling/intervals" and inform the user for an app update.

https://angular.io/guide/service-worker-getting-started

The service worker can detect every 1minutes for example if there are new "assets/your app" and do a slide up to inform the user to reload the page or even force it if you want to.

Godber answered 19/4, 2020 at 17:38 Comment(0)
P
1

Good Morning! I had the same problem when assembling the application using angular 11. To solve it, just run as follows.

"ng build --named-chunks"

Provencher answered 24/1, 2022 at 16:20 Comment(2)
This leads to caching problems on the clients browser. As the names of newly deployed JS-files aren't change anymore, the browser will still use the old (cached) files.Approval
Not if you set the files to not be cached on the webserver.Finnougrian
B
0

TL;DR use npm run ... to serve your app, not ng serve ...

I was getting this error ...

screenshot of error

 Error: Uncaught (in promise): ChunkLoadError: Loading chunk default-projects_app3-orders_src_app_order_order_component_ts failed.
(error: http://localhost:4205/default-projects_app3-orders_src_app_order_order_component_ts.js)
__webpack_require__.f.j@http://localhost:4205/remoteOrders.js:50599:29

... while going through a module federation worked example. Turned out I wasn't serving the various bits correctly. I had read the scripts section of package.json...

  "scripts": {
...
    "start:app1": "ng serve",
    "start:app2": "ng serve app2-restaurant",
    "start:app3": "ng serve app3-orders",
...
  }

... and wrongly assumed I could just run ng serve, etc. at the command line.

Once my colleague pointed out I should instead run npm run start:app1, etc., it started working properly.

Bagworm answered 12/10, 2022 at 4:6 Comment(0)
S
0

For some reason the pattern: const chunkFailedMessage = /Loading chunk [\d]+ failed/; wasn't working for me. I had to change it to: const chunkFailedMessage = /Loading chunk .*failed.*[.js\\)]/;

Below the full ErrorHandler for Angular 17+:

import { ErrorHandler, Injectable } from "@angular/core";

@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
    handleError(error: any): void {
        const chunkFailedMessage = /Loading chunk .*failed.*[.js\\)]/;
        if (chunkFailedMessage.test(error.message)) {
            if (confirm("New version available. Load New Version?")) {
                window.location.reload();
            }
        }
    }
}

The way I use it is as follows: so for example you have multiple tabs open of your application and in one of them you logout. In my case this meant the token was expired or invalidated (SSO). So the other tab wasn't aware of this, but because the modules were lazy-loaded and you need a valid token for that, to load modules as you go, I got this: Loading Chunk .... failed ... .js message. In the application a notification is showed and user is sent to the login page.

Samathasamau answered 12/3 at 9:17 Comment(0)
L
-1

You can send some event from server side to reload the application. Also there is option to pre fetch the lazy modules in background so as to prefetch them as soon as possible instead of waiting for the request of that modules.

Lorgnon answered 9/3, 2018 at 17:49 Comment(2)
Actually I think using PreloadAllModules is the key as this means they get cached in the browser so it then doesn't matter if there's been a change uploaded as they will still have access to the cached version.Dolerite
preloading all modules is a bad idea if speed matters to your users and you have a big SPA. One of the benefits of chunking is that parts of it can be fetched as and when they are needed, which speeds up the loading time of applications.Deadwood
L
-1

Use Pre-Loading. You get the benefits of lazy loading, without the hassle it causes in situations like this. All of the chunks will be given to the user as fast as possible without slowing down the initial load time. Below is an excerpt from https://vsavkin.com/angular-router-preloading-modules-ba3c75e424cb to explain how it works (see the article for diagrams):

First, we load the initial bundle, which contains only the components we have to have to bootstrap our application. So it is as fast as it can be.

Then, we bootstrap the application using this small bundle.

At this point the application is running, so the user can start interacting with it. While she is doing it, we, in the background, preload other modules.

Finally, when she clicks on a link going to a lazy-loadable module, the navigation is instant.

We got the best of both worlds: the initial load time is as small as it can be, and subsequent navigations are instant.

Leesaleese answered 13/8, 2019 at 16:37 Comment(1)
Links to external resources are encouraged, but please add context around the link so your fellow users will have some idea what it is and why it’s there. Always quote the most relevant part of an important link, in case the target site is unreachable or goes permanently offline. See: How to anwser.Bricole
G
-11

Clear Browser cache. Worked for me

Giantism answered 20/6, 2019 at 23:40 Comment(1)
A "normal" website user would never clear his browser cache.Approval

© 2022 - 2024 — McMap. All rights reserved.