Markus Dresch
Markus Dresch

Reputation: 5574

Angular 6: Observable async binding not working as expected after HttpErrorResponse

I'm trying to handle errors in angular globally using an ErrorHandler as layed out here: https://medium.com/@aleixsuau/error-handling-angular-859d529fa53a

I'm forwarding the error messages to a notification service. The app component template is bound to an observable provided by the service using async pipe.

When a client error is thrown, everything works as expected: The error is caught, notification is sent and the UI is displaying the error message. After 3 seconds the message disappears, since the observable changes to a null value.

On HttpErrorResponses the behavior is strange: The error is caught, notification is sent, but the UI does not update. Unless another HttpErrorResponse is thrown within 3 seconds!

Am i missing something or is this a bug in Angular 6 or RxJs?

I created a minimal, complete and verifiable example on stackblitz: https://stackblitz.com/edit/angular-e9keuw

The ErrorHandler:

@Injectable()
export class ErrorSink implements ErrorHandler {

    // ErrorHandler is created before the providers
    // we have to use the Injector to get them
    constructor(private injector: Injector) {}

    handleError(error: Error | HttpErrorResponse) {
        console.error('Caught error: ', error);

        const notificationService = this.injector.get(NotificationService);

        // client error
        if (!(error instanceof HttpErrorResponse)) {
            console.log('client error!');
            return notificationService.notify(error.message);
        }

        // offline error
        if (!navigator.onLine) {
            console.log('No Internet Connection');
            return notificationService.notify('No Internet Connection');
        }

        // http error
        console.log(error.status, error.message);
        return notificationService.notify(`${error.status} - ${error.message}`);
    }
}

The NotificationService:

@Injectable()
export class NotificationService {

  private subject: BehaviorSubject<string> = new BehaviorSubject(null);
  readonly notification$: Observable<string> = this.subject.asObservable();

  constructor() {}

  notify(message) {
    console.log('notification', message)

    this.subject.next(message);
    setTimeout(() => this.subject.next(null), 3000);
  }
}

The Component:

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {

  constructor(
    private notificationService: NotificationService,
    private http: HttpClient
  ) {}

  throwError(): void {
    throw new Error('an error was thrown!');
  }

  loadUrl() {
    this.http.get('https://www.google.com/').subscribe(
      data => console.log(data)
    );
  }
}

And the bound template:

<div *ngIf="notificationService.notification$ | async as notification">
  {{ notification }}
</div>

Upvotes: 1

Views: 815

Answers (1)

Poul Kruijt
Poul Kruijt

Reputation: 71891

The reason is that the error is being triggered outside the zone. I do not know the exact reason why that happens, because I don't see all your code, but it is :). Update your NotificationService to run the notify inside the zone:

@Injectable()
export class NotificationService {

  private subject: BehaviorSubject<string> = new BehaviorSubject(null);
  readonly notification$: Observable<string> = this.subject.asObservable();

  private timeout: number = 0;

  constructor(readonly zone: NgZone) {}

  notify(message) {
    console.log('notification', message)

    clearTimeout(this.timeout);

    this.zone.run(() => {
      this.subject.next(message);
      this.timeout = setTimeout(() => this.subject.next(null), 3000);
    });
  }
}

One hint though, save your setTimeout ref in a class member. This way you can cancel the setTimeout if you have two errors within 3 seconds. It could happen that the second error cannot be read because it's already set to null

Upvotes: 5

Related Questions