Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to handle/inform users about unrecoverable exceptions in an APP_INITIALIZER?

My Angular5 app loads a config file from the backend during application initialization (APP_INITIALIZER). Since the app cannot be run without it, my goal is to show a message to the user that the config couldn't be loaded.

 providers: [ AppConfig,
        { provide: APP_INITIALIZER, useFactory: (config: AppConfig) => () => config.load(), deps: [AppConfig], multi: true },
        { provide: LocationStrategy, useClass: HashLocationStrategy},
        { provide: ErrorHandler, useClass: GlobalErrorHandler }]

The AppConfig class should load a config file from a backend service before the app loads:

@Injectable()
export class AppConfig {

    private config: Object = null;
    constructor(private http: HttpClient) {}

    public getConfig(key: any) {
        return this.config[key];
    }


    public load() {
        return new Promise((resolve, reject) => {
            this.http.get(environment.serviceUrl + 'config/config')
                .catch((error: any) => {                  
                    return Observable.throw(error || 'Server error');
                })
                .subscribe((responseData) => {
                    this.config = responseData;
                    this.config['service_endpoint'] = environment.serviceUrl;                  
                    resolve(true);
                });
        });
    }
}

Global Exception handler:

@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
    constructor( private messageService: MessageService) { }
    handleError(error) {
        // the AppConfig exception cannot be shown with the growl message since the growl component is within the AppComponent
        this.messageService.add({severity: 'error', summary: 'Exception', detail: `Global Exception Handler: ${error.message}`});

        throw error;
    }

}

If the config file cannot be loaded, an exception is thrown, caught & rethrown in the global exception handler (uncaught HTTPErrorResponse in console.log()) and the loading spinner is hanging for ever)

Since the AppComponent does not get loaded (which is ok, since the app cannot be used without the configuration) and as my message/"growl" Component is a sub component of the AppComponent, I cannot display a message to the user.

Is there a way to show a message in the index.html page during this stage? I wouldn't like to redirect the user to a different .html page than the index.html, since the users will then just reload/f5 on the error.html.

like image 722
netik Avatar asked Feb 01 '18 14:02

netik


3 Answers

In main.ts I have changed bootstrap in this way:

platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => {
    // error should be logged into console by defaultErrorLogger, so no extra logging is necessary here
    // console.log(err);
    // show error to user
    const errorMsgElement = document.querySelector('#errorMsgElement');
    let message = 'Application initialization failed';
    if (err) {
        if (err.message) {
            message = message + ': ' + err.message;
        } else {
            message = message + ': ' + err;
        }
    }
    errorMsgElement.textContent = message;
});

any occurred exceptions are caught and message is set into an html element. The element is defined in index.html in app-root tag, e.g.

<app-root><div id="errorMsgElement" style="padding: 20% 0; text-align: center;"></div> 
</app-root>

The content of tag is replaced by angular, if bootstrap has been successful.

like image 70
Jirka Pressfreund Avatar answered Feb 16 '23 10:02

Jirka Pressfreund


I haven't found a good solution for a very similar issue too, but as a workaround, this is how I did it:

  • Extend the config class with an initialized variable:

    @Injectable()
    export class AppConfig {       
        public settings: AppSettings = null;
        public initialized = false;
    
        public load() {
            return new Promise((resolve, reject) => {
                this.http.get(environment.serviceUrl + 'config')
                    .catch((error: any) => {                      
                        this.initialized = false;
                        resolve(error);
                        return Observable.throw(error || 'Server error');
                    })
                    .subscribe((responseData: any) => {
                        this.settings = responseData;
                        this.initialized = true;
                        resolve(true);
                    });
            });
        }
    }
    
  • And within the app.component.html I show either the app or an error message, depending on this variable:

    <div *ngIf="!appConfig.initialized">
        <b>Failed to load config!</b>
        <!-- or <app-config-error-page> -->
    </div>
    <div *ngIf="appConfig.initialized" #layoutContainer >
        <!-- the app content -->
    </div>
    
  • The global exception handler:

    @Injectable()
    export class GlobalErrorHandler implements ErrorHandler {
        constructor( private notificationService: NotificationService) { }
        handleError(error) {
            this.notificationService.error('Exception', `${error.message}`);
            return Observable.throw(error);
        }  
    }
    

Of course one could also store the concrete error message / exception text in the config object and show it to the users in the app.component/error.page.component if needed.

like image 26
user66875 Avatar answered Feb 16 '23 09:02

user66875


I have a slightly different approach to offer. In my opinion, we do not want the bootstrap of the Angular app to continue if we can't properly initialize it, since it won't work properly. Instead, we just want to display message to the user, and stop.

Background: in my initialization code, I have to make two asynchronous requests in a particular order:

  1. I make an HTTP request to my "own" web server (where the Angular app is served from), to get the hostname of second web server that hosts my back-end API.

  2. I then make a request to that second web server to retrieve further configuration information.

If either of those requests fail, then I need to stop and show a message rather than Angular continuing with the bootstrap process.

Here's a simplified version of my code before adding in the new exception handling (note that I prefer to use Promise and async/await rather than Observable, but that's not really relevant):

const appInitializer = (
  localConfigurationService: LocalConfigurationService,
  remoteConfigurationService: RemoteConfigurationService
): () => Promise<void> => {

  return async () => {
    try {
      const apiHostName = await localConfigurationService.getApiHostName();
      await remoteConfigurationService.load(apiHostName);
    } catch (e) {
      console.error("Failed to initialize the Angular app!", e);
    }
};

export const appInitializerProvider = {
  provide: APP_INITIALIZER,
  useFactory: appInitializer,
  deps: [LocalConfigurationService, RemoteConfigurationService],
  multi: true
};

That code will log the error to the console, but then continue with the bootstrap process - not what we want.

To modify this code to (a) display a message, and (b) stop the bootstrap process in its tracks, I added the following 3 lines immediately after that console.error call:

      window.document.body.classList.add('failed');

      const forever = new Promise(() => { }); // will never resolve
      await forever;

The first line simply adds the class 'failed' to the document <body> element. We'll see what effect that has in a moment.

The other two lines await a Promise that is never resolved - in other words, they wait forever. This has the effect of stalling the Angular bootstrap process, so the Angular app never appears.

The final change is in my index.html file (the HTML file that's loaded by the browser, and which "contains" the Angular app). Here's what it looked like before my changes:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>My App</title>
  <base href="/">
</head>
<body>
  <app-root>Loading...</app-root>
</body>
</html>

That "loading..." text within the <app-root> element is displayed while the Angular app is initialized, and is replaced with the app content once the bootstrap process is complete.

To ensure that an "oops!" message is shown if the app cannot be initialized, I've added a <style> block with some CSS styles, and some extra content within the <app-root> element:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>My App</title>
  <base href="/">

  <style type="text/css">
    #failure-message {
      display: none;
    }

    body.failed #loading-message {
      display: none;
    }

    body.failed #failure-message {
      display: block;
    }
  </style>

</head>
<body>
<app-root>
  <h1 id="loading-message">Loading...</h1>
  <h1 id="failure-message">Oops! Something went wrong.</h1>
</app-root>
</body>
</html>

So now, the "loading..." message is displayed by default (to be replaced with the app content if initialization is successful), but the "oops!" message is displayed instead if the <body> element has the failed class - which is what we add in our exception handler.

Of course, you can do better than a simple <h1> tag - you can put any content you like in there.

The net effect of all this is that the browser displays "loading..." and then either the Angular app loads or the "loading..." text is replaced with "oops!".

Note that the URL isn't changed, so if you hit reload in the browser, it'll start from scratch and try to load the Angular app all over again - which is probably what you want.

like image 31
Gary McGill Avatar answered Feb 16 '23 10:02

Gary McGill