Guidelines for effectively managing and notifying users of unrecoverable exceptions during APP_INITIALIZER execution

During the initialization of my Angular5 app, it retrieves a configuration file from the backend using APP_INITIALIZER. If the app fails to load this configuration file, I want to display a message to the user indicating the issue.

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

For the AppConfig class to function properly, it needs to fetch the configuration file from the backend service before the app can fully load:

@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 displayed 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 app fails to load the config file, it triggers an exception which is caught and handled by the global exception handler resulting in an uncaught HTTPErrorResponse in the console.log(), while the loading spinner continues indefinitely.

Since the AppComponent is not loaded (which is expected as the app relies on the configuration), and my message/growl Component is a subcomponent of the AppComponent, I am unable to display a message to the user.

Is it possible to show a message on the index.html page at this stage? I prefer not to redirect the user to a different .html page as they would simply refresh on the error.html page.

Answer №1

Within my main.ts file, I made a modification to the bootstrap function as follows:

platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => {
    // The error is automatically logged by defaultErrorLogger, so no additional logging is needed here
    // console.log(err);
    // Display error to the 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 exceptions that occur are captured and the corresponding message is inserted into an HTML element. This element is specified within the app-root tag in the index.html file, for example:

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

The content of this tag is replaced by Angular once the bootstrap process is successful.

Answer №2

I have encountered a similar issue and while there isn't a perfect solution, I managed to come up with a workaround. Here is how I approached it:

  • To address the issue, I added an initialized variable to the config class:

    @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);
                    });
            });
        }
    }
    
  • In the app.component.html, I display either the app content or an error message based on the initialized 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>
    
  • I also implemented a 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);
        }  
    }
    

Additionally, it is possible to store the specific error message or exception text in the config object and display it to users in the app.component or error page component as required.

Answer №3

My perspective offers a unique approach. I firmly believe that the Angular app's bootstrap should not proceed if initialization is unsuccessful, as it will lead to malfunctioning. Instead, it is better to inform the user with a message and halt the process.

Context: In my setup, the initialization involves making two sequential asynchronous requests:

  1. First, an HTTP request is made to my "own" web server (where the Angular app is hosted) to acquire the hostname of the second web server that hosts the backend API.

  2. Subsequently, a request is sent to the second web server to fetch additional configuration data.

If either of these requests fails, I believe it is essential to display a message and prevent Angular from continuing its bootstrap process.

Below is a simplified version of my code prior to incorporating the new exception handling technique:

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
};

While this code logs an error to the console, it proceeds with the bootstrap process, which is not the desired behavior.

To address this issue and effectively halt the bootstrap process while displaying a message, I introduced the following three lines immediately after the console.error call:

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

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

The first line adds the class 'failed' to the <body> element of the document. The subsequent lines await a Promise that remains unresolved indefinitely, effectively pausing the Angular bootstrap process and preventing the app from loading.

The final modification is made in the index.html file, the HTML file loaded by the browser and housing the Angular app. Prior to my changes, it appeared as follows:

<!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>

The text "Loading..." within the <app-root> element indicates the ongoing initialization process, to be replaced by the app content upon completion of the bootstrap.

To ensure the display of an "oops!" message in case of initialization failure, I included a <style> block with CSS styles and additional 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"><br><br>
    #failure-message {
      display: none;
    }

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

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

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

Now, by default, the "Loading..." message is displayed and replaced by app content upon successful initialization, while the "oops!" message appears if the <body> element includes the failed class, as set in our exception handling code.

One can certainly enhance the <h1> tag with more engaging content to convey the status effectively.

In summary, this approach ensures that the browser exhibits "loading..." initially, with the Angular app loading thereafter or displaying the "oops!" message in case of initialization issues.

Note that the URL remains unchanged; therefore, if the page is reloaded, the Angular app restarts the loading process from the beginning, reflecting the intended behavior.

Answer №4

Instead of throwing toGlobalErrorHandler, consider updating your initializer catch block

   this.http.get(environment.apiUrl + 'settings/config')
        .catch((error: any) => {
            window.location.href = '/path/another-page.html';
            return Observable.throw(error || 'An error occurred on the server')
        })
        .subscribe((responseData) => {
            this.configData = responseData;
            this.configData['endpoint'] = environment.apiUrl;
            resolve(true);
        });

Answer №5

Here are the steps I follow...

// app.module.ts

export function fetchAppConfiguration(http: HttpClient) {
  return async () => {
    try {
      const config = await http
          .get<IAppConfiguration>(`//${window.location.host}/appconfig.json`)
          .toPromise();
      // `appConfig` is a global object of type IAppConfiguration
      Object.assign(appConfig, config); 
      return config;
    }
    catch (err) {
      alert('An error occurred with the message: ' + err.message);
      throw err;
    }
  };
}

@NgModule({
  // ...
  providers: [
    // ...
    { 
      provide: APP_INITIALIZER, 
      useFactory: fetchAppConfiguration, 
      multi: true, 
      deps: [HttpClient]
    },
  ]
})
export class MainModule { ... }

I hope this approach proves useful :-)

Answer №6

To perform a redirect, first create an error.html file within the src folder. Once the file is created, redirect the user to that file in case of an error.

After setting up the error.html file, modify your angular.json file to include it in the assets section. This ensures that the angular application does not load and redirects instead.

 "assets": [
       {
        "glob": "**/*",
        "input": "src/assets",
        "output": "/assets"
       },
       {
         "glob": "error.html",
         "input": "src",
         "output": "/"
       }
    ],

Update your load function as follows:

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

Answer №7

One approach I took was to set up an interceptor along with an error service. This setup enables the interceptor to capture all HTTP errors that fall within specific status codes known to be errors.

Example of Interceptor

import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpErrorResponse } from '@angular/common/http';
import { Router } from '@angular/router';

import { ErrorService } from '@app/@pages/error/error.service';

import { catchError } from 'rxjs/operators';
import { Observable, throwError } from 'rxjs';

@Injectable()
export class HttpErrorInterceptor implements HttpInterceptor {

    constructor(
        private router: Router,
        private errorSvc: ErrorService
    ) { }

    intercept(req: HttpRequest<any>, next: HttpHandler) {
        return next
            .handle(req)
            .pipe(
                catchError((error: any, resp: Observable<HttpEvent<any>>) => {
                    if (error instanceof HttpErrorResponse && (error.status === 0 || error.status > 400)) {
                        this.errorSvc.setHttpError(error);
                        this.router.navigate(['/error']);
                    }
                    return throwError(error);
                })
            );
    }
}

Example of Error Service

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

import { LocalStorageService } from 'angular-2-local-storage';

import { BehaviorSubject, Observable } from 'rxjs';
import { HttpErrorResponse } from '@angular/common/http';

@Injectable({
    providedIn: 'root'
})
export class ErrorService {

    private error$: BehaviorSubject<HttpErrorResponse> = new BehaviorSubject(this.localStorageSvc.get<HttpErrorResponse>('error'));

    constructor(private localStorageSvc: LocalStorageService) { }

    setHttpError(error: HttpErrorResponse): void {
        this.localStorageSvc.set('error', error);
        this.error$.next(error);
    }

    getHttpError(): Observable<HttpErrorResponse> {
        return this.error$.asObservable();
    }
}

To illustrate, the interceptor captures errors when loading configuration data via APP_INITIALIZER. Once an error is caught, the application will be directed to an error page. The error service leverages local storage and rxjs Behavior Subject to manage the error state. The error component will then utilize this service in its constructor and subscribe to display error details.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

New feature in Chrome allows credit card suggestions to be displayed in the name input field

On one specific page of my angular app, I have an input field for a name that is showing credit card suggestions. When I disable the autofill for credit cards in Chrome settings, it then shows suggestions for names instead. However, on another page with ...

Instead of displaying the name, HTML reveals the ID

I have defined a status enum with different values such as Draft, Publish, OnHold, and Completed. export enum status { Draft = 1, Publish = 2, OnHold = 3, Completed = 4 } In my TypeScript file, I set the courseStatus variable to have a de ...

Sharing API types from a NestJs backend to a Vue frontend (or any other frontend) application: Best practices

In my development setup, I have a VueJs and Typescript Front End app along with a PHP server that is in the process of being converted to NestJs. Both of these projects reside in the same Monorepo, allowing me to share types for DTOs between them. I am ex ...

A guide on implementing nested child routes in AngularJS 2

I have successfully completed routing for two children, but now I want to display nested routes for those children. For example: home child1 child2 | grand child | grand child(1) ...

Get the download URL from Firebase Storage and save it into an array within Firestore

I am currently working on a project to upload multiple image files to Firebase Storage and then store their download URLs in a single array within Firestore. uploadImages(name, images) { for (let i = 0; i < images.length; i++) { const file = ...

What are the steps to implement a Bottom Navigation bar in iOS and a Top Navigation bar in Android using NativeScript Angular?

For my project, I decided to utilize the tns-template-tab-navigation-ng template. I am currently working on creating a WhatsApp clone, and in iOS, it features a bottom navigation bar while in Android, it has a top navigation bar. I am looking for guidance ...

Centralized Vendor Repository for Managing Multiple Angular2 Applications

Looking to host different Angular2 apps that share the same framework packages and node_modules across a main domain and subdomains: domain.com subdomain.domain.com sub2.domain.com Directory Layout public_html ├── SUBDOMAINS │ ├── sub2 ...

Alert for timeout triggered without any error logs recorded

While working on a login & register page, I encountered an issue. After clicking the register button, a timeout alert error appeared despite no log errors being present. However, the data used to register was successfully saved in the MYSQL database. ...

Interacting Components in Angular 2/5

I am facing an issue with my guard-service where I need to change the member of my app.component. To achieve this, I have created a setter in app.component and it should be executed by the guard-service using Input and Output. import {AppComponent} from & ...

Ensuring Consistency in Array Lengths of Two Props in a Functional Component using TypeScript

Is there a way to ensure that two separate arrays passed as props to a functional component in React have the same length using TypeScript, triggering an error if they do not match? For instance, when utilizing this component within other components, it sh ...

Is there a way to prompt the compiler to generate an error when a type cannot be set to `undefined`

I'm working with a type called ILoadedValue<TValue>, which acts as a wrapper for types that can potentially be undefined. However, I want to prevent situations where TValue cannot be undefined, as it's more efficient to use the actual value ...

Accessing properties in TypeScript using an index that is determined by another property within the same interface

Allow me to illustrate my query with an example rather than simply using words: interface Y { x: number, y: string } interface PairValue<T> { key: keyof T, value: T[this['key']] } const pairValue: PairValue<Y> = { ...

Sharing cookies between the browser and server in Angular Universal

After trying to integrate cookies in browser and server using the example from this link, I encountered an issue. The cookies do not appear to be shared between the browser and the server. The cookie I set on the browser side is visible, but when I try to ...

The interfaced JSON object did not return any defined value

I am working on implementing a pipe in Angular 9 that will handle the translation of my table words into different languages. To achieve this, I have set up a JSON file with key-value pairs for translation services and created corresponding interfaces for ...

Tips for parsing a JSON file within an Ionic app?

I need assistance with reading a JSON file in my Ionic app. I am new to this and unsure about how to proceed. In this scenario, there is a provider class where I fetch data from import { Injectable } from '@angular/core'; import { Http } from ...

What causes an error during the compilation of an Angular package containing a singleton class?

I am currently in the process of creating an Angular library. Within this library, I have developed a singleton class to manage the same SignalR connection. Here is the code implementation: import * as signalR from '@microsoft/signalr'; export c ...

Are two requests sent by Angular Http Options and Delete?

Currently, I am attempting to send a request to the server in order to remove an object based on its ID. The backend system being utilized is Web Api. Angular removable: string = 'http://localhost:49579/api/resource/remove/28'; this._http ...

What sets apart the states of the select tag from other input tags in Angular?

I am having trouble displaying an error message for a select tag when it is in a touched state. The error handling seems to be working fine for other input tags, but not for the select tag. Below is the code snippet: <div class="form-g ...

Converting an IEnumerable of XElement into a List of Objects within an IActionResult: A Comprehensive Guide

I am struggling to figure out how to return a List in my ASP.NET Core CRUD method. It needs to return List<BookItem>. The search part of the method seems to be working fine as it returns a list of IEnumerable<XElement> that contains strings, I ...

Unable to retrieve information from a Node.js server using Angular 2

I am currently learning Angular 2 and attempting to retrieve data from a Node server using Angular 2 services. In my Angular component, I have a button. Upon clicking this button, the Angular service should send a request to the server and store the respo ...