Angular: Why don't subscriptions to a Subject in my custom ErrorHandler fire?

Viewed 38

I have an Angular app with a custom ErrorHandler. My goal is to allow components (say, the AppComponent) to be notified when a global error occurs so that they can present it in the interface. I implemented this by extending ErrorHandler (as described in the documentation), and I added a Subject property called onError$ which emits a value when handleError() is called. I then inject ErrorHandlerService into the app component, and it subscribes to onError$. Here's a minimal repro (also a Stackblitz here):

Repro

error-handler.service.ts

import { ErrorHandler, Injectable } from '@angular/core';
import { Subject } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class ErrorHandlerService implements ErrorHandler {
  onError$ = new Subject<string>();
  
  handleError(error: any): void {
    this.onError$.next(`An error: ${error}`);
    console.log("Hi from ErrorHandlerService! I called next().");
  }
}

app.module.ts

import { ErrorHandler, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { ErrorHandlerService } from './error-handler.service';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  providers: [{ provide: ErrorHandler, useClass: ErrorHandlerService }],
  bootstrap: [AppComponent]
})
export class AppModule { }

app.component.ts

import { Component, OnInit } from '@angular/core';
import { ErrorHandlerService } from './error-handler.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.less']
})
export class AppComponent implements OnInit {
  title = "SO Angular Error Question";

  constructor(private errorService: ErrorHandlerService) { }

  ngOnInit(): void {
    this.errorService.onError$.asObservable().subscribe(error => {
      console.log("Hi from the subscription to the onError$ subject!", error);
    });

    throw new Error('Method not implemented.');
  }
}

Expected result

When I manually throw the error in app.component.ts, I expect the console.log statement in ErrorHandlerService to run, and then I expect the subscription callback in AppComponent to run its console.log, resulting in two messages being printed to the console from my app.

Observed result

The console.log from ErrorHandlerService happens, but the one in AppComponent's subscription callback doesn't.

First console log statement runs, but the second doesn't

My question

Why does the callback for the subscription in AppComponent not occur?

This question seems related, but it doesn't have an accepted answer, and adding .asObservable() before subscribing in the component doesn't resolve the problem. This question is also related and "solves" the problem by calling a method on an error reporting service explicitly in the error handler, but I'm still curious why my solution doesn't work.

My suspicion is that despite the fact that my ErrorHandlerService is marked with providedIn: 'root', the framework creates an instance to act as the global error handler, and the AppComponent is receiving a second one, but I can't find documentation about this behavior anywhere. Any help appreciated!

1 Answers

Cool question, nicely formatted

tl;dr

//in app.component.ts
constructor(@Inject(ErrorHandler) private errorService: ErrorsService,

This is a (uncommon) dependancy injection problem

In your app.module.ts you have

providers: [{ provide: ErrorHandler, useClass: ErrorsService }],

The token is ErrorHandler, the provided thing is ErrorsService

When you inject a service in your constructor like

constructor(private errorService: ErrorsService

That's shorthand for

constructor(@Inject(ErrorsService) private errorService: ErrorsService 

Here the token is ErrorsService, the provided thing is ErrorsService

Angular looks for providers of tokens to fulfill the DI, so the fix is to use the right token.

Related