Why does toaster service inside Global Error Handler wont work in Angular4/5/6?

5.5k Views Asked by At

My requirement is to load some data by calling Two Rest Api's before app component loads.If API gives any error display the message in Toaster (angular2-toaster).

Before loading app component the below AppLoadService executes

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import 'rxjs/add/operator/toPromise';
import { APP_SETTINGS } from 'app/app-settings/settings';

@Injectable()
export class AppLoadService {

constructor(private httpClient: HttpClient) { }

loadLabs(): Promise<any> {
    return new Promise((resolve, reject) => {
        this.httpClient.get(`/api/v1/api/lab`)
            .toPromise()
            .then((res: any) => {
                APP_SETTINGS.labs = res;
                resolve();
            })
            .catch((err: any) => {
                reject(err);
            });
    });
}
/////////////////******************////////////////////////////
getSettings(): Promise<any> {
    return new Promise((resolve, reject) => {
        this.httpClient.get(`assets/settings/config.json`)
            .toPromise()
            .then((config: any) => {
                APP_SETTINGS.loginURL = config["login"];
                console.log(`config.json loaded:: `, APP_SETTINGS);
                resolve();
            })
            .catch((err: any) => {
                reject(err);
            });
    });
 }

My App module file is like below

    export function createTranslateLoader(http: Http) {
        return new TranslateStaticLoader(http, './assets/i18n', '.json');
    }


    @NgModule({
    declarations: [
        AppComponent, CustomDateRangePickerComponent
    ],
    imports:  [
            // coreModules: //
            BrowserModule,
            BrowserAnimationsModule,
            ToasterModule,
            HttpClientModule,
            FormsModule,
            CommonModule, //<====added

            //thirdPartyModules:
            // ToastyModule,
            BootstrapModalModule,
            //appModules: //
            AppRoutingModule,
            FooterModule,
            ErrorModule,
            AccessDeniedModule,
            NotFoundModule,
            AppLoadModule, //Startupdata before APP loaded
            RouterModule.forRoot([]),
            TranslateModule.forRoot({ provide: TranslateLoader, useFactory: (createTranslateLoader), deps: [Http] })
    ],
    providers: [
        ToasterService,
        StartupService,
        ResponseStatusesService,
        LocalStorageService,
        ApplicationSettingsService,
        LabSelectionService,
        AccountService,
        AuthService,
        AlertService,
        AuthGuard,
        RolesGuard,
        FeaturebasedGuard,
        ErrorLogService,
        {
            provide: ErrorHandler,
            useClass: GlobalErrorsHandler
        },
        {
            provide: HTTP_INTERCEPTORS,
            useClass: AppHttpInterceptor,
            multi: true
        },
        {
            provide: LocationStrategy,
            useClass: HashLocationStrategy
        },
    ],
    exports: [TranslateModule],
    bootstrap: [AppComponent]
    })

    export class AppModule { }

GlobalErrorHandler.ts

@Injectable()
export class GlobalErrorsHandler extends ErrorHandler {
constructor(
    private injector: Injector,
    private errorLogService: ErrorLogService

) {
    super();
    alert('GlobalErrorsHandler');

}
handleError(error: Error | HttpErrorResponse) {
    debugger;
    let toaster = this.injector.get(ToasterService);
    toaster.pop("head", "body");
}
}

AppComponent.html

<toaster-container [toasterconfig]="ang2toasterconfig"></toaster-container>

<router-outlet></router-outlet>

Same issue with interceptors as well

            import { Injectable, Injector } from '@angular/core';
            import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse, HttpErrorResponse } from '@angular/common/http';
            import { Observable } from 'rxjs/Observable';
            import 'rxjs/add/operator/do';
            import 'rxjs/add/operator/catch';
            import 'rxjs/add/observable/throw';
            import { ToasterService } from 'angular2-toaster';
            import { AuthService } from 'app/blocks/auth/auth.service';
            import { TranslateService } from 'ng2-translate';
            import { AlertService } from '../../core/services/common';


            @Injectable()
            export class AppHttpInterceptor implements HttpInterceptor {

                constructor(private injector: Injector) { }

                intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
                    debugger;
                    console.log(req);
                    if (!window.navigator.onLine) { // check to see if there's internet
                        // if there is no internet, throw a HttpErrorResponse error
                        // since an error is thrown, the function will terminate here
                        return Observable.throw(new HttpErrorResponse({ error: 'NO_INTERNET' }));
                    } else {
                        // else return the normal request
                        return this.handleResponse(next, req);
                    }


                }
                handleResponse(next, req) {
                    return next.handle(req)
                        .do((ev: HttpEvent<any>) => {
                            if (ev instanceof HttpResponse) {
                            }
                        })
                        .catch((response: any) => {
                            if (response instanceof HttpErrorResponse) {
                                console.log('response in the catch: ', response);
                                this.handleReponseExceptions(response);
                            }
                            return Observable.throw(response);
                        });
                }
                handleReponseExceptions(exception) {
                    let toaster = this.injector.get(ToasterService);
                    let translate = this.injector.get(TranslateService);

                    // TOASTER NOT WORKING AND TRANSLATE NOT WORKING
                    toaster.pop("test","test");

                    this.translate.get(["test", "test"]).subscribe(res => {
                        //NOT FETCHING FROM en.json
                    });

                }

            }

As per my knowledge, the toaster.pop('','') method is called before toaster-container loaded. How to solve this issue. Root component is the App component, where I placed the toaster container. Please suggest me the architecture to solve this issue.

One more, where I need to handle different errors... In Local Errorhandler (component level) or in Global error handler or in Interceptor?

Example errors: 400,404,500+ ...etc

Update1: As per the David comment changed code like below, but still No Container ..... message is coming in console and no toaster is visible

Using "angular2-toaster": "^6.1.0"

enter image description here

enter image description here

These are the API calls which will be fired before app component enter image description here

enter image description here

5

There are 5 best solutions below

0
On

This happens because handleError() method is executed outside of the Angular zone. This causes toasts not to behave correctly since change detection doesn't run on it. Turn on onActivateTick in the error handler to ensure that the toast is running inside Angular's zone:

Working Example

@Injectable()
export class GlobalErrorHandler extends ErrorHandler {

    constructor(@Inject(Injector) private readonly injector: Injector) {}

    handleError(error) {
        console.log("Handling error: " + error);
        this.toastrService.error("testing", null, { onActivateTick: true })
    }

    /**
     * Need to get ToastrService from injector rather than constructor injection to avoid cyclic dependency error
     * @returns {} 
     */
    private get toastrService(): ToastrService {
        return this.injector.get(ToastrService);
    }

}

Git Issue

0
On

This is example works in Angular 6

app.component.html

<toaster-container [toasterconfig]="toasterconfig"></toaster-container>
<router-outlet></router-outlet>

app.component.ts

import { ToasterConfig } from 'angular2-toaster';
import { CustomToasterService} from '../app/core/_services/custom-toaster.service';

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.scss'],
    providers: [CustomToasterService],
})
export class AppComponent implements OnInit {
    toasterConfig: any;
    toasterconfig: ToasterConfig = new ToasterConfig({
        positionClass: 'toast-bottom-right',
        showCloseButton: true,
        tapToDismiss: false,
        timeout: 5000
    });
    constructor(public settings: SettingsService) {
    }

custom-toaster.service.ts

import { Injectable } from '@angular/core';
import { ToasterService } from 'angular2-toaster';

@Injectable()
export class TooasterService {
    constructor(
        private toasterService: ToasterService,
    ) { }


    public showError(error) {
        this.toasterService.pop('error', error.name, error.Message); 
    }
}
0
On

Customer error handler will create before the tosterservice as the But while creating error handler the roster service is not available

To resolve the issue use Injector in constructor in custom error handler and in handleerror method get tosterservice using injector

2
On

This is documented in angular2-toaster's README due to the number of times this occurs and the confusion it causes.

This is caused by how Angular handles UI dispatching (or rather, the lack thereof) in error handlers.

The handleError function is executed outsize of an Angular zone. You need to explicitly tell Angular to run the pop call within the context of a zone.

export class AppErrorHandler implements ErrorHandler {
    constructor(
        private toasterService: ToasterService,
        private ngZone : NgZone) { }

    handleError(error: any): void {
        this.ngZone.run(() => {
            this.toasterService.pop('error', "Error", error);
        });  
    }
}
1
On

I added Java Script Toaster not angular and shown the Text.