async/await in Angular `ngOnInit`

66,032

Solution 1

It is no different than what you had before. ngOnInit will return a Promise and the caller will ignore that promise. This means that the caller will not wait for everything in your method to finish before it proceeds. In this specific case it means the view will finish being configured and the view may be launched before this.data is set.

That is the same situation you had before. The caller would not wait for your subscriptions to finish and would possibly launch the app before this.data had been populated. If your view is relying on data then you likely have some kind of ngIf setup to prevent you from accessing it.

I personally don't see it as awkward or a bad practice as long as you're aware of the implications. However, the ngIf can be tedious (they would be needed in either way). I have personally moved to using route resolvers where it makes sense so I can avoid this situation. The data is loaded before the route finishes navigating and I can know the data is available before the view is ever loaded.

Solution 2

Now, obviously, Angular will not “know”, that ngOnInit has become async. I feel that this is not a problem: My app still works as before.

Semantically it will compile fine and run as expected, but the convenience of writing async / wait comes at a cost of error handling, and I think it should be avoid.

Let's look at what happens.

What happens when a promise is rejected:

public ngOnInit() {
    const p = new Promise((resolver, reject) => reject(-1));
}

The above generates the following stack trace:

core.js:6014 ERROR Error: Uncaught (in promise): -1
    at resolvePromise (zone-evergreen.js:797) [angular]
    at :4200/polyfills.js:3942:17 [angular]
    at new ZoneAwarePromise (zone-evergreen.js:876) [angular]
    at ExampleComponent.ngOnInit (example.component.ts:44) [angular]
    .....

We can clearly see that the unhandled error was triggered by a ngOnInit and also see which source code file to find the offending line of code.

What happens when we use async/wait that is reject:

    public async ngOnInit() {
        const p = await new Promise((resolver, reject) => reject());
    }

The above generates the following stack trace:

core.js:6014 ERROR Error: Uncaught (in promise):
    at resolvePromise (zone-evergreen.js:797) [angular]
    at :4200/polyfills.js:3942:17 [angular]
    at rejected (tslib.es6.js:71) [angular]
    at Object.onInvoke (core.js:39699) [angular]
    at :4200/polyfills.js:4090:36 [angular]
    at Object.onInvokeTask (core.js:39680) [angular]
    at drainMicroTaskQueue (zone-evergreen.js:559) [<root>]

What happened? We have no clue, because the stack trace is outside of the component.

Still, you might be tempted to use promises and just avoid using async / wait. So let's see what happens if a promise is rejected after a setTimeout().

    public ngOnInit() {
        const p = new Promise((resolver, reject) => {
            setTimeout(() => reject(), 1000);
        });
    }

We will get the following stack trace:

core.js:6014 ERROR Error: Uncaught (in promise): [object Undefined]
    at resolvePromise (zone-evergreen.js:797) [angular]
    at :4200/polyfills.js:3942:17 [angular]
    at :4200/app-module.js:21450:30 [angular]
    at Object.onInvokeTask (core.js:39680) [angular]
    at timer (zone-evergreen.js:2650) [<root>]

Again, we've lost context here and don't know where to go to fix the bug.

Observables suffer from the same side effects of error handling, but generally the error messages are of better quality. If someone uses throwError(new Error()) the Error object will contain a stack trace, and if you're using the HttpModule the Error object is usually a Http response object that tells you about the request.

So the moral of the story here: Catch your errors, use observables when you can and don't use async ngOnInit(), because it will come back to haunt you as a difficult bug to find and fix.

Solution 3

I wonder what are the downsides of an immediately invoked function expression :

    ngOnInit () {
      (async () => {
        const data = await this.service.getData();
        this.data = this.modifyMyData(data);
      })();
    }

It is the only way I can imagine to make it work without declaring ngOnInit() as an async function

Solution 4

I used try catch inside the ngOnInit():

async ngOnInit() {      
   try {           
       const user = await userService.getUser();
    } catch (error) {           
        console.error(error);       
    }    
} 

Then you get a more descriptive error and you can find where the bug is

Solution 5

You can use rxjs function of.

of(this.service.getData());

Converts the promise to an observable sequence.

Share:
66,032

Related videos on Youtube

qqilihq
Author by

qqilihq

Co-founder and Cee-something-Ohh at LineUpr: Create an app for virtual, hybrid and physical events in thirty minutes. Developer of the Selenium KNIME Nodes: Automate your browser with a graphical workflow without writing a line of code. One of the developers behind NodePit: Your search engine for KNIME nodes and workflows.

Updated on February 27, 2022

Comments

  • qqilihq
    qqilihq about 2 years

    I’m currently evaluating the pros ‘n’ cons of replacing Angular’s resp. RxJS’ Observable with plain Promise so that I can use async and await and get a more intuitive code style.

    One of our typical scenarios: Load some data within ngOnInit. Using Observables, we do:

    ngOnInit () {
      this.service.getData().subscribe(data => {
        this.data = this.modifyMyData(data);
      });
    }
    

    When I return a Promise from getData() instead, and use async and await, it becomes:

    async ngOnInit () {
      const data = await this.service.getData();
      this.data = this.modifyMyData(data);
    }
    

    Now, obviously, Angular will not “know”, that ngOnInit has become async. I feel that this is not a problem: My app still works as before. But when I look at the OnInit interface, the function is obviously not declared in such a way which would suggest that it can be declared async:

    ngOnInit(): void;
    

    So -- bottom line: Is it reasonable what I’m doing here? Or will I run into any unforseen problems?

    • ConnorsFan
      ConnorsFan about 5 years
      According to this comment in issue 17420: "it's not a problem for someone to use async ngOnInit, it is just an awkward/not recommended coding practice."
    • qqilihq
      qqilihq about 5 years
      @ConnorsFan I’ve actually read exactly this issue before opening my post :-) (should have linked it). I am still not sure, whether “awkward” and “not recommended” have any objective reasons, or whether the Angular team just wants to push towards the reactive style?
    • ConnorsFan
      ConnorsFan about 5 years
      Here is another good read on this subject.
    • Lee Gunn
      Lee Gunn about 4 years
      @qqilihq - Hi, I'm looking at doing this conversion just now. Did you go ahead with it and were you happy with the outcome? Any issues...?
    • qqilihq
      qqilihq about 4 years
      @LeeGunn It’s a so-so. I used it here and there, but all in all very sparingly. Reason (a) being the error handling (explained in detail below by @Reactgular), (b) code gets a little less “nested” (no callbacks), but the gain is quite small, and (c) we’re working with several “real” Observables in the codebase (which continuously update) -- there await will not help, and we’d end up with two inconsistent ways (or confuse new team members).
  • qqilihq
    qqilihq over 4 years
    Thanks. But my goal was to avoid Observable in favor of the await syntax (I've grown a strong aversion against callback-based syntax, back in the days :-) ).
  • Sonu Kapoor
    Sonu Kapoor about 4 years
    Good explanation.
  • Raven
    Raven almost 4 years
    The missing part here is how would the stacktrace look when the error happens in the Observable are they any better?
  • Raven
    Raven almost 4 years
    @qqilihq i agree, Promise was ugly, then we got await and async and i was happy, now we have Observable and we are back to ugly but more useful than Promise.
  • Reactgular
    Reactgular almost 4 years
    @Peter most observables that throw an error are usually from HttpClient and are asynchronous. So when the error is thrown the stacktrace will be from outside of your source code. You can use the catchError() operator to handle errors, and if you just want stacktrace help, then catchError(err => throwError(err)) would rethrow the error and add your source code to the call stack.
  • RyuCoder
    RyuCoder almost 4 years
    async ngOnInit(): Promise<any> did the trick for me.
  • Leogout
    Leogout over 3 years
    Thank you for mentionning route resolvers. I think it is a better approach to this problem that occurs very often.
  • ABajpai
    ABajpai about 3 years
    but this is as good as having a separate async function and calling it within ngOnInit ngOnInit () { this.fetchData(); } private async fetchData(){ const data = await this.service.getData(); this.data = this.modifyMyData(data); }
  • Victor Valeanu
    Victor Valeanu about 3 years
    @ABajpai , yes, but without having a separate async function and without making ngOninit() async, which is what the user seemed to ask for.
  • Dan King
    Dan King about 3 years
    "In this specific case it means the view will finish being configured and the view may be launched before this.data is set." - this is the key I think, and it's not obvious unless you think about it, and is likely to be even less obvious to future developers who are trying to maintain your code. For this reason I would agree with the comment referenced above that this is in fact "awkward" and "bad practice".
  • qqilihq
    qqilihq over 2 years
    Resolvers are a concept to consider, but they’re not a silver bullet. There are situations where onInit is desirable. (e.g. less code scattering, showing a loading indicator in the target view, …)
  • Jonathan
    Jonathan over 2 years
    it is the same thing as just running the promise without await, it will work, but not wait for it to finish
  • qqilihq
    qqilihq over 2 years
    This has already been suggested before. Please see my answer here: stackoverflow.com/a/69147601/388827
  • Rob Hall
    Rob Hall about 2 years
    Febuary 2022 and Observable.toPromise() is now deprecated. Working in Angular 13 and using SSR/prerendering I found this solution to be fully featured and work out of the box.
  • Jonathan
    Jonathan about 2 years
    @Rob Hall - I updated it to use firstValueFrom, as that is the new version. Sometimes you should use lastValueFrom instead. The recommended solution you listed is extremely complicated. My version is simplified. - J
  • Rob Hall
    Rob Hall about 2 years
    @jonathan agreed. That version also requires injection so it is not as straight forward to work into an existing algorithm.