6

I'm trying to implement a custom ExceptionHandler in an Angular 2 app which submits uncaught errors to a custom AlertsService. The goal is to allow the main App component to subscribe to the alerts provided by the AlertsService so that it can display the errors in the UI.

The problem I'm seeing is that errors submitted to the AlertsService by the custom ExceptionHandler are not reflected in the UI until another error is encountered. This causes the UI to always be one alert behind what is actually being provided by the AlertsService.

My guess is that this behavior has something to do with change detection and the special case of the ExceptionHandler, but I'm not sure where to go from here. Looking to the Angular2 experts for help!

Sample code below, plunk here:

import { Component, ExceptionHandler, Injectable, OnInit, provide } from '@angular/core';
import { bootstrap } from '@angular/platform-browser-dynamic';
import { Subject } from 'rxjs/Subject'

export interface Alert {
  message: string;
}

@Injectable()
export class AlertsService {

  private alertTriggeredSubject = new Subject<Alert>();

  alertTriggered = this.alertTriggeredSubject.asObservable();

  triggerAlert(message: string) {
    this.alertTriggeredSubject.next(<Alert>{ message: message });
  }

}

@Injectable()
export class CustomExceptionHander {

  constructor(private alertsService: AlertsService) { }

  call(exception, stackTrace = null, reason = null) {
    this.alertsService.triggerAlert(exception.originalException);
    console.error('EXCEPTION:', exception);
  }
}

@Component({
  selector: 'child-component',
  template : `
  <h3>Child</h3>
  <div id="child">
    <button (click)="breakMe()">Break Me!</button>
    <div>Alerts Sent:</div>
    <ul><li *ngFor="let error of errors">{{error}}</li></ul>
  </div>`
})
export class ChildComponent {

  errors: string[] = [];
  numErrors = 0

  breakMe() {
    this.numErrors++;
    let error = `I broke it (${this.numErrors})`;

    // The error added to the array below is never reflected in the 
    // "Alerts Sent:" <ul>...not sure why
    this.errors.push(error);
    console.info('ChildComponent.errors', this.errors);

    // Simulate unhandled exception
    throw new Error(error);
  }
}

@Component({
  selector: 'my-app',
  template : `
  <h3>Parent</h3>
  <div id="parent">
    <div>Alerts Received:</div>
    <ul><li *ngFor="let alert of alerts">{{alert.message}}</li></ul>
    <child-component></child-component>
  </div>`
  directives: [ChildComponent]
})
export class App implements OnInit {

  constructor(private alertsService: AlertsService) { }

  alerts: Alert[] = [];

  ngOnInit() {
    this.alertsService.alertTriggered.subscribe(alert => {
      this.alerts.push(alert);

      // Alert gets received, but is not reflected in the UI
      // until the next alert is received, even thought the 
      // alerts[] is up-to-date.
      console.info('App alert received:', alert);
      console.info('App.alerts:', this.alerts);
    });
  }
}

bootstrap(App, [
    AlertsService,
    provide(ExceptionHandler, { useClass: CustomExceptionHander })
]).catch(err => console.error(err));

1 Answer 1

11

update ExceptionHandler was renamed to ErrorHandler https://stackoverflow.com/a/35239028/217408

orgiginal

Change detection isn't run at the end of the click event when the handler throws.

You can invoke change detection manually but this gets a bit complicated because you need an ApplicationRef reference and ApplicationRef depends on ExceptionHandler which makes a neat cycle and DI can't resolve cyclic dependencies.

A workaround is to instead of ApplicationRef inject the Injector and acquire AplicationRef imperatively like

constructor(private alertsService: AlertsService, injector:Injector) { 
  setTimeout(() => this.appRef = injector.get(ApplicationRef));
}

and then in call invoke change detection like

call(exception, stackTrace = null, reason = null) {
  this.alertsService.triggerAlert(exception.originalException);
  this.appRef.tick();
  console.error('EXCEPTION:', exception);
}

Plunker example

Sign up to request clarification or add additional context in comments.

9 Comments

Thanks for the quick response. So, if I understand this correctly, you updated the exception handler to manually trigger change detection to handle cases where change detection wasn't triggered because of the exception? Not exactly intuitive, but it works.
I guess it is safe to do an extra change detection cycle after an exception. It shouldn't be too frequently ;-) There might be a better way. I'm not an expert in custom exception handler implementation.
One follow on question before I mark this accepted. Also, in the button click handler the error is pushed to the ChildComponent's errors[]. This value is never updated in the UI even though we're now manually run change detection in the ExceptionHandler. Any idea why?
I missed that part. I couldn't find a way to make this component update. This might be worth a bug report. I don't know what the expected behavior here is.
I saw a comment to an issue recently where they say that exceptions need to be handled locally where they appear. ExceptionHandler is just for unhandled exceptions for central error logging. You shouldn't rely on your application still working properly after the ExceptionHandler was invoked.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.