4

In our Angular app we need to load config settings from the server before initializing the app.

Using APP_INITIALIZER as such works great:

export function loadConfig(config: ConfigService) => () => config.load()

@NgModule({
    declarations: [AppComponent],
    imports: [BrowserModule,
        routes,
        FormsModule,
        HttpModule],
    providers: [
        ConfigService,
        {
            provide: APP_INITIALIZER,
            useFactory: loadConfig,
            deps: [ConfigService],
            multi: true
        }
    ],
    bootstrap: [AppComponent]
})
export class AppModule {
}

I then extended ErrorHandler to create a GlobalErrorHandler and wanted to tell Angular to use our GlobalErrorHandler, so I provided the ErrorHandler and told Angular to use the class GlobalErrorHandler.

    providers: [
            ConfigService,
            {
                provide: APP_INITIALIZER,
                useFactory: loadConfig,
                deps: [ConfigService],
                multi: true
            },        
            {
                provide: ErrorHandler,
                useClass: GlobalErrorHandler
            }
    ]

GlobalErrorHandler needs information from the config settings and I need to instantiate GlobalErrorHandler AFTER the setting's promise is resolved. In the set up above, GlobalErrorHandler is created before my call is made to the server for the configuration values.

How can I load the configuration values from the server, and THEN instantiate the GlobalErrorHandler and use the configuration values?

Thibs
  • 8,058
  • 13
  • 54
  • 85

2 Answers2

5

It doesn't seem to be possible with the way you want it done. Here is the relevant code that instantiates the ErrorHandler and calls APP_INITIALIZER callbacks:

  private _bootstrapModuleFactoryWithZone<M>(moduleFactory: NgModuleFactory<M>, ngZone?: NgZone):
      Promise<NgModuleRef<M>> {
    ...
    return ngZone.run(() => {
      const ngZoneInjector =
          ReflectiveInjector.resolveAndCreate([{provide: NgZone, useValue: ngZone}], this.injector);
      const moduleRef = <InternalNgModuleRef<M>>moduleFactory.create(ngZoneInjector);

      !!! ------------- here ErrorHandler is requested and instantiated ------------ !!!
      const exceptionHandler: ErrorHandler = moduleRef.injector.get(ErrorHandler, null);
      if (!exceptionHandler) {
        throw new Error('No ErrorHandler. Is platform module (BrowserModule) included?');
      }
      ...
      return _callAndReportToErrorHandler(exceptionHandler, () => {
        const initStatus: ApplicationInitStatus = moduleRef.injector.get(ApplicationInitStatus);

       !!! ------------ and here your APP_INITIALIZER callbacks are executed ---------- !!!
        initStatus.runInitializers();
        return initStatus.donePromise.then(() => {
          this._moduleDoBootstrap(moduleRef);
          return moduleRef;
        });
      });
    });
  }

As you can see the ErrorHanlder is requested from the injector before your callbacks are triggered. And there's no other way to perform server request before.

I believe your only option is to make a request before the application starts and then once you get the response add the ErrorHanlder to module providers and bootstrap the app:

// app.module.ts
export class AppModule {}

export const decoratorDescriptor = {
  imports: [BrowserModule, FormsModule, ReactiveFormsModule],
  declarations: [AppComponent, DebounceDirective, ExampleComponent],
  providers: [],
  bootstrap: [AppComponent]
};


// main.ts
const http = injector.get(Http);
http.get('assets/configs/configuration.json').subscribe((r) => {
  const ErrorHandler = configureErrorHandler(r.json());
  decoratorDescriptor.providers.push(ErrorHandler);
  const decorated = NgModule(decoratorDescriptor)(AppModule);
  platformBrowserDynamic().bootstrapModule(decorated);
});

See this answer to learn how to configure the http.

Max Koretskyi
  • 101,079
  • 60
  • 333
  • 488
  • Thank you for your answer. Many years later, I'm also facing a similar issue and tried your approach. When I do it like this, then angular does not find the components anymore to compile the templates: [ng] Error: src/app/app.component.html:1:1 - error NG8001: 'ion-app' is not a known element Is your approach not working anymore with newer angular versions (angular 13) or do you have another hint for me, how to solve this issue? – stofl Mar 30 '22 at 22:07
3

Thought the answer might help someone who needs it...

I was able to resolve the issue by giving the APP_INITIALIZER as a dependency in the GlobalErrorHandler section. This made the GlobalErrorHandler instantiation to wait till the APP_INITIALIZER was available, thus solving my issue.

providers: [
            ConfigService,
            {
                provide: APP_INITIALIZER,
                useFactory: loadConfig,
                deps: [ConfigService],
                multi: true
            },        
            {
                provide: ErrorHandler,
                useClass: GlobalErrorHandler,
                deps: [APP_INITIALIZER]
            }
    ]
Prasanth Mohan
  • 285
  • 1
  • 2
  • 12
  • This did not work for me, Angular still runs the Error Handler provider first (which makes sense given the code from Max's answer) – RocketMan Sep 09 '20 at 12:45
  • Same here, the global error handler still fires first, and I have several dependencies being injected into the constructor that are all null when I add "deps: [APP_INITIALIZER]" – David Hoffman Jan 09 '22 at 18:08