18

I'm using angular 2 lazy routing. Client is bundled with webpack2 using AOT and angular-router-loader to lazy load children. Everything works as expected when the browser is connected, that is I can successfully navigate the router to the lazy loaded module, the chunk is loaded successfully and I can view the component etc.

However if I emulate being disconnected (e.g by using the Offline chrome developer tools option) routing fails, as expected, because it can't load the relevant chunk. The error is 'Loading chunk [chunk number] failed'

After that no routing command works, it's like the router is broken.

I tried to handle the error using a global ErrorHandler. The idea was that perhaps I could catch the error before it breaks the router, but it seems that it is too late by then. At the time I catch the error the router is not working.

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

import { Router } from '@angular/router';

@Injectable()
export class CustomErrorHandler extends ErrorHandler {
    constructor(private injector: Injector) {
        super(false);
    }

    private get router(): Router {
        return this.injector.get(Router, null);
    }

    public handleError(error: Error) {
        if (/Loading chunk [\d]+ failed/.test(error.message)) {
            console.info('Offline mode');
            let router = this.router;
            if (router) {
                router.navigateByUrl('/offline').then(t => console.debug(t+'')).catch(t=> console.debug(t));
            } else {
                window.location.href = '/';
            }
        }
    }
}

The Custom error handler works because the 'Offline mode' message is printed. Injection also works, router is not null, however router navigation does not work, the router promise is not resolved neither rejected.

What I'm trying to accomplish is to handle the error (e.g. to show an informative message to the user) and at the same time having the router at a working state, so that the user can navigate later (when internet connection is restored) without reloading the whole page.

Update 1: trying to reproduce without aot and webpack

In order to see if this is an angular router issue I tried to see what happens when trying to work offline with jit compilation. I used the : angular router plunker navigated to login tab, switched to offline and pressed login. A number of 'Error: XHR error loading' errors were logged as the client was trying to load the admin module. Ultimately navigation failed, however routing navigation did not break after that. I was able to navigate to other tabs and after switching back online I was even able to navigate to admin. Perhaps the problem is how angular-router-loader webpack plugin tries to load the module.

Update 2: Seems to be a known issue

Feature: more robust module loader for router

angeor
  • 181
  • 1
  • 6

2 Answers2

9

I was getting this error every time I walked my dog and tried to test my website in the elevator. Amazed there isn't better documentation for handling errors - and especially by all the non solutions in the github issue.

With that said, there is a NavigationError event from router. So you can intercept that, prevent your normal error handler from running and show a popup. The nice thing about this event is it contains the full attempted URL - instead of a useless Error: Uncaught (in promise): Error: Loading chunk common failed. error.

So here's what I came up with.

In app.component.ts (must inject private router: Router)

Call the initErrorHandler() function from your constructor.

// filter router events of type NavigationError
navigationError$ = this.router.events.pipe
                   (filter((e): e is NavigationError => e instanceof NavigationError));

// Keep a reference to the error handled, so we can ignore it later
_errorHandled: any;
initErrorHandler()
{
   this.navigationError$.subscribe((evt) => 
   {
       let showPopup = false;

       // handle known error messages that can be retried
       if (evt.error.message.startsWith('Loading chunk')) 
       {
           showPopup = true;
       }
       else 
       {
          debugger;   // consider handling other messages
       }

       if (showPopup) {

          // display a popup however you want...
          // I use Angular Material, and popup is MY OWN dialog box service
          this.popup.showChoicesDialog({

              title: 'Network Error',
              desc: 'Sorry we were unable to load that page',
              fullscreen: true,

              choices: [
                  {
                      action: () => 
                      {
                          // this is the important part - retry URL
                          this.router.navigateByUrl(evt.url)
                      },
                      buttonText: 'Retry'
                  }
              ]
          });

          // mark the error as handled to avoid global handler
          this._errorHandled = evt.error;
      }

      // normal global error handler
      this.zone.onError.subscribe((error: any) => 
      {
          // ignore errors that were already handled but couldn't be cancelled
          if (error.rejection === this._errorHandled)
          {
              this._errorHandled = undefined;
              return;
          }

          // Whatever you want to do here for true unhandled errors
          console.warn(error); 
      });
  }

Depending upon your user base you'd need to vary the message and behavior but the general approach seems to be working.

I don't yet know the full scope of all possible NavigationError, so right now this just handles Loading chunk errors. Please comment if you find alternative messages errors.

Be sure to take advantage of the 'Offline' mode when testing. In quick testing if I enable 'Offline' mode to trigger the error, then try to navigate again it does indeed successfully navigate once I've disabled 'Offline'.

enter image description here

Note: A lot of discussion topics about 'chunk errors' are related to caching issues after a new deployment. I have not yet addressed those issues, but it's likely the error message will differ such that you could show a different popup.

Shane
  • 659
  • 1
  • 9
  • 19
Simon_Weaver
  • 140,023
  • 84
  • 646
  • 689
  • 1
    I've also had some success with this plugin too which reattempts downloading of chunks due to certain network issued https://www.npmjs.com/package/webpack-retry-chunk-load-plugin - seems to have quite dramatically reduced loading failures, most of which I suspect are phone users with spotty coverage. – Simon_Weaver Apr 10 '21 at 19:32
1

Prevent the routing failure rather than handling it.

You could preload your lazy loaded modules so they are loaded to the client once the browser detects inactivity, that way they would be available in offline mode.


Check here to preload all modules: PreloadAllModules


You could also create your custom loading strategy and preload only selected modules:

custom-preloading.ts
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/mergeMap';
import { PreloadingStrategy, Route } from '@angular/router';

export class CustomPreloading implements PreloadingStrategy {

   public preload(route: Route, fn: () => Observable<boolean>): Observable<boolean> {
     if (route.data && route.data.preload) {
       return Observable.of(true).flatMap((_: boolean) => fn());
     } else {
       return Observable.of(false);
     }
   }
}
app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { CustomPreloading } from './custom-preloading';

const routes: Routes = [
  { ..other paths.. },
  { path: 'lazy', loadChildren: 'app/lazy/lazy.module#LazyModule', data: { preload: true }  }
];

export const appRoutingProviders = [
   CustomPreloading
];

@NgModule({
   imports: [RouterModule.forRoot(routes, { preloadingStrategy: CustomPreloading })],
   exports: [RouterModule]
})
export class AppRoutingModule { }