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));
Alexander Abakumov
  • 13,617
  • 16
  • 88
  • 129
WayneC
  • 2,530
  • 3
  • 31
  • 44

1 Answers1

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

Community
  • 1
  • 1
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • 1
    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. – WayneC Jun 13 '16 at 17:24
  • 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. – Günter Zöchbauer Jun 13 '16 at 17:26
  • 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? – WayneC Jun 13 '16 at 17:27
  • 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. – Günter Zöchbauer Jun 13 '16 at 17:46
  • 1
    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. – Günter Zöchbauer Jun 15 '16 at 13:14
  • @GünterZöchbauer do you know how to achieve this using Angular 5? Seems that [`ApplicationRef`](https://angular.io/api/core/ApplicationRef) became an interface and the `Injector` fails with something like `Uncaught Error: StaticInjectorError[ApplicationRef]: NullInjectorError: No provider for ApplicationRef!`... – davidenke Nov 09 '17 at 14:24
  • I haven't seen anything related in the changelog and in the source it's still a class https://github.com/angular/angular/blob/5.0.x/packages/core/src/application_ref.ts#L364 – Günter Zöchbauer Nov 09 '17 at 14:28
  • The example in the plunker shows a single change detection tick, it doesn't do more than one. See this forked plunker example where UI changes don't happen after the first exception (press do it after break me): [plunker](https://plnkr.co/edit/FJ3CskXeudz3MekC5QIR?p=preview) – Igor Nadj Jan 31 '18 at 04:41
  • You shouldn't assume the app to be in a working state after the exceptiin handler was called. You can send information to a server about the exception, you can inform the user that the app crashed and needs to be reloaded and then reload. The exception handler is not supposed to keep the app running. – Günter Zöchbauer Jan 31 '18 at 04:47