Search code examples
angularngrx

Ngrx Store as dependency in providers causes cyclic dependency


I'm trying to override default angular ErrorHandler by providing factory function which should have ngrx store injected:

import { ErrorHandler } from "@angular/core";

[...]

    {
      provide: ErrorHandler,
      useFactory: (store: Store<AppState>) => {
        return Sentry.createErrorHandler({
          showDialog: true,
          dialogOptions: {},
        });
      },
      deps: [Store],
    },

but I get cyclic dependency error:

main.ts:42 Error: NG0200: Circular dependency in DI detected for Store. Find more at https://angular.io/errors/NG0200
    at throwCyclicDependencyError (core.js:216)
    at R3Injector.hydrate (core.js:11434)
    at R3Injector.get (core.js:11257)
    at injectInjectorOnly (core.js:4751)
    at ɵɵinject (core.js:4755)
    at injectArgs (core.js:4832)
    at Object.factory (core.js:11522)
    at R3Injector.hydrate (core.js:11438)
    at R3Injector.get (core.js:11257)
    at injectInjectorOnly (core.js:4751)

How to omit this problem? I need to provide sth from the store to the factory function that creates error handler (Sentry.createErrorHandler).


Solution

  • Simplest way Ive found is to implement a custom Error Handler as ngrx-store uses Angulars EventHandler eg

    SentryErrorHandler

    import { ErrorHandler, Injectable } from "@angular/core";
    import * as Sentry from "@sentry/angular";
    import { Integrations } from "@sentry/tracing";
    
    @Injectable()
    export class SentryErrorHandler implements ErrorHandler {
      constructor() {
        Sentry.init({
          dsn: "https://[your-sentry-dsn-here]",
          integrations: [
           new Integrations.BrowserTracing({
             tracingOrigins: ["localhost", "other-test-urls"],
             routingInstrumentation: Sentry.routingInstrumentation
          })
        ]
      });
    }
    
    handleError(error: Error) {
        Sentry.captureException(error);
      }
    }
    

    then in your app.module

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

    To test an example would be to create an incorrect url in an Effect that calls a service and NOT catchError to see if it pops up in Sentry eg

    @Injectable()
    export class SettingsEffects {
      constructor(private actions$: Actions, 
       private settingsService: settingsService) {
    
      } 
    
     loadSettings$ = createEffect(() => this.actions$
      .pipe(
        ofType(settingsActions.loadSettings),
        mergeMap(() => this.settingsService.get()
         .pipe(
           map((response) => settingsActions.loadSettingsSuccess({ payload: response})),
          //catchError((error) => of(settingsActions.loadSettingsFailed({ payload: error}))) //commented out should go to Sentry instead
        ))
      ));
    }
    

    UPDATE Seems the ErrorHandler is created very early in the lifecycle before the providers so tried by adding the Injector as a constructor parameter

    @Injectable()
    export class SentryErrorHandler implements ErrorHandler {
      //store property to use in handleError
      private get _store() {
       return this.injector.get(Store);
      }
    
      constructor(private injector: Injector) {
      }.......
    

    see this answer Injecting services in custom ErrorHandler causes "Cannot instantiate cyclic dependency!" error, how can I fix this?