Is it a good practice using Observable with async/await?
Asked Answered
T

4

95

I am using angular 2 common http that return an Observable, but I face with a problem that my code likes a mesh when I use nested Observable call:

this.serviceA.get().subscribe((res1: any) => {
   this.serviceB.get(res1).subscribe((res2: any) => {
       this.serviceC.get(res2).subscribe((res3: any) => {

       })
   })
})

Now I wanna use async/await to avoid that but async/await only work with Promise. I know that Observable can be converted to Promise but as I know, it is not a good practice. So what should I do here ?

BTW, it will be nice if anyone can give me an example code to solve this with async/await :D

Trainee answered 1/4, 2018 at 9:25 Comment(5)
if you are not using any of the features of observables you might get away with it. As for it beeing good practice, probably not.Heriberto
However, nesting subscription is bad practice : you come bakc to the callback hell that Promises and Observable are supposed to help preventing in the first placeNadean
If you want to chain Observables, use flatMap / switchMap operators. this.serviceA.get().flatMap((res1: any) => this.serviceB.get()).flatMap((res2: any) => this.serviceC.get()).subscribe( (res3: any) => { .... });Nadean
My thoughts on Promises & observables is there tools for achieving different goals & not an alternative to each other. E.g.. promises are great for processing a set of actions that need doing, observables are great for were you want multiple subscribers been notified. And of course mixing them were appropriate makes total sense.Udall
IHMO one shouldn't use observables for one-time actions and a single subscriber at all. That's exactly what promises with async/await are for (and they are FAR more readable).Silvanasilvano
N
115

Chaining Observables in sequence, as you want to do in your code

Concerning your code example, if you want to chain Observables (trigger another after the previous emits), use flatMap (or switchMap) for this purpose :

this.serviceA.get()
  .flatMap((res1: any) => this.serviceB.get())
  .flatMap((res2: any) => this.serviceC.get())
  .subscribe( (res3: any) => { 
    .... 
  });

This one is better practice compared to nesting, as this will make things clearer and help you avoid callback hell, that Observable and Promises were supposed to help preventing in the first place.

Also, consider using switchMap instead of flatMap, basically it will allow to 'cancel' the other requests if the first one emits a new value. Nice to use if the first Observable that triggers the rest is some click event on a button, for instance.

If you don't need your various requests to wait in turn for each other, you can use forkJoin or zip to start them all at once, see @Dan Macak answer's for details and other insights.


Angular 'async' pipe and Observables work well together

Concerning Observables and Angular, you can perfectly use | async pipe in a Angular template instead of subscribing to the Observable in your component code, to get the value(s) emitted by this Observable


ES6 async / await and Promises instead of Observables ?

if you're not feeling using Observable directly, you can simply use .toPromise() on your Observable, and then some async/await instructions.

If your Observable is supposed to return only one result (as it is the case with basic API calls) , an Observable can be seen as quite equivalent to a Promise.

However, I'm not sure there is any need to do that, considering all the stuff that Observable already provide (to readers : enlightening counter-examples are welcome!) . I would be more in favor of using Observables whenever you can, as a training exercise.


Some interesting blog article on that (and there are plenty of others):

https://medium.com/@benlesh/rxjs-observable-interop-with-promises-and-async-await-bebb05306875

The toPromise function is actually a bit tricky, as it’s not really an “operator”, rather it’s an RxJS-specific means of subscribing to an Observable and wrap it in a promise. The promise will resolve to the last emitted value of the Observable once the Observable completes. That means that if the Observable emits the value “hi” then waits 10 seconds before it completes, the returned promise will wait 10 seconds before resolving “hi”. If the Observable never completes, then the Promise never resolves.

NOTE: using toPromise() is an antipattern except in cases where you’re dealing with an API that expects a Promise, such as async-await

(emphasis mine)


The example you requested

BTW, it will be nice if anyone can give me an example code to solve this with async/await :D

Example if you really want to do it (probably with some mistakes, can't check right now, please feel free to correct)

// Warning, probable anti-pattern below
async myFunction() {
    const res1 = await this.serviceA.get().toPromise();
    const res2 = await this.serviceB.get().toPromise();
    const res3 = await this.serviceC.get().toPromise();
    // other stuff with results
}

In the case you can start all requests simultaneously, await Promise.all() which should be more efficient, because none of the calls depends on the result of each other. (as would forkJoin do with Observables)

async myFunction() {
    const promise1 = this.serviceA.get().toPromise();
    const promise2 = this.serviceB.get().toPromise();
    const promise3 = this.serviceC.get().toPromise();

    let res = await Promise.all([promise1, promise2, promise3]);

    // here you can retrieve promises results,
    // in res[0], res[1], res[2] respectively.
}
Nadean answered 1/4, 2018 at 9:35 Comment(11)
actually I just simply wan to use promise that provided from Http in @angular/http, but this module does not support interceptor (because I use JWT), so I have to use @angular/common/http that return Observable. It causes my problem.Trainee
It will be nice if someone will add Observable zip and forkJoin examples.It can be used when requests can be done in parallel, e.g. nested subscribtions does not need data from prev requests.Caliche
About your last code snippet, actually, it would be better to use await Promise.all() with all those call, otherwise they would have to wait for previous ones before even firing the request.Tucson
Yes. Promise.all and Observable.zip (forkJoin).Caliche
forkJoin and zip are good ideas, the former even more as it emits only the last values of the inner Observables. I will elaborate on that in an answer.Tucson
@DanMacák I agree that in this example, the request don't need to wait on each other because none depends on the result of the previous. however, I wanted to give an example where one could actually use the previous result, which could be the case (you get some object info, with some id in it you do another call to another API, etc...). As per the nested subscription, OP could have used res1 in second call, res2 in second call, etc... I didn't want to break this potential use.Nadean
@Caliche yes, I'd like that too ! It seems Dan Macák is writing one, so I won't clutter my answer even more :)Nadean
@Nadean glad to help, especially with such an interesting topic.Tucson
Actually I have to use res1 for serviceB.get() call, so I think I can use your flatMap solutionTrainee
@ThachHuynh Gald it helped, and thank you for feedbackNadean
@Nadean grats for the great answer. I would just complete your last sample with how to retrieve values from Promise.all, using let res = await Promise.all(...) into res[0] / res[1] / res[2].Clarisclarisa
T
33

As @Pac0 already elaborated on the various solutions well, I will just add slightly different angle.

Mixing Promises and Observables

I personally prefer not mixing Promises and Observables - which is what you get while using async await with Observables, because even though they look similar, they are very different.

  • Promises are always async, Observables not necessarily
  • Promises represent just 1 value, Observables 0, 1 or many
  • Promises have very limited use, you can't eg. cancel them (put aside ES next proposals), Observables are so much more powerful in their use (you can manage for example multiple WS connections with them, try that with Promises)
  • Their APIs differ greatly

Use of Promises in Angular

Now even though it is sometimes valid to use both, especially with Angular I think one should consider going as far with RxJS as possible. The reasons being:

  • Great portion of Angular API uses Observables (router, http ...), so one kind of goes with and not against the stream (no pun intended) by using RxJS, otherwise one would have to convert to Promises all the time while making up for the lost possibilities RxJS provides
  • Angular has powerful async pipe which allows for composing your whole application data flow of streams which you filter, combine and do whatever modification you want on it without interrupting the stream of data coming from server without a single need for thening or subscribing. This way, you don't need to unwrap the data or assign it to some auxiliary variables, the data just flows from services through Observables straight to the template, which is just beautiful.

There are some cases though where Promise still can shine. For example what I am missing in rxjs TypeScript types is concept of single. If you are creating an API to be used by others, returning Observable is not all that telling: Will you receive 1 value, many, or will it just complete? You have to write comment to explain it. On the other hand, Promise has much clearer contract in this case. It will always resolve with 1 value or reject with error (unless it hangs forever of course).

Generally, you definitely don't need to have only Promises or only Observables in your project. If you just want to express with a value that something was completed (deleting user, updating user), and you want to react on it without integrating it to some stream, Promise is the more natural way of doing so. Also, using async/await gives you the power to write code in sequential manner and therefore simplifying it greatly, so unless you need advanced management of incoming values, you can stay with Promise.


Back to your example

So my recomendation is to embrace both the power of RxJS and Angular. Coming back to your example, you can write the code as following (credits for the idea to @Vayrex):

this.result$ = Observable.forkJoin(
  this.serviceA.get(),
  this.serviceB.get(),
  this.serviceC.get()
);

this.result$.subscribe(([resA, resB, resC]) => ...)

This piece of code will fire 3 requests and once all of those request Observables have completed, subscription callback to forkJoin will get you the results in an array, and as said, you can subscribe to it manually (as in the example) or do this declaratively using result$ and async pipe in the template.

Using Observable.zip would get you the same result here, the difference between forkJoin and zip is that the former emits only last values of inner Observables, the latter combines first values of the inner Observables, then second values etc.


Edit: Since you need the results of previous HTTP requests, use flatMap approach in @Pac0's answer.

Tucson answered 1/4, 2018 at 11:25 Comment(4)
Actually I have to use resA for serviceB.get() call. Which is the best way to do it with Observable ?Trainee
It is the flatMap approach as in the Pac0s example (the first snippet).Tucson
Second parameter to subscribe is error callback, or if you don't subscribe explicitly or want to have more freedom in error handling, you can call catch after forkJoin with error handler as first parameter. There you can map error to other value for example.Tucson
source$.switchMap(x => promiseDelay(x)) // works .subscribe(x => console.log(x));Denunciatory
S
11

Since toPromise is deprecated now in 2022. I want to show another way of using await on an observable. I find this method to make much more readable code, as opposed to long, complex rxjs pipes. This is especially useful for http requests since there is only one response, and you generally want to wait for the response before doing something else.


Update

My initial solution works, but rxjs has basically the same function: firstValueFrom().

From the docs:

async function execute() {
  const source$ = interval(2000);
  const firstNumber = await firstValueFrom(source$);
  console.log(`The first number is ${firstNumber}`);
}

Original Solution

If you have an observable, you can wrap it in a promise, subscribe, and resolve when the subscription emits.

getSomething(): Promise<any> {
    return new Promise((resolve, reject) => {
      this.http
        .get('www.myApi.com')
        .subscribe({
          next: (data) => resolve(data),
          error: (err) => reject(err),
        });
    });
  }

Now we can wait for a response inside of an async function

  async ngOnInit() {
    const data = await this.getSomething();
    //Do something with your data
  }

Now we can perform plenty of complex operations on the data and it will be much more readable to people who aren't rxjs wizards. If you had three subsequent http requests that relied on each other it would look like:

  async ngOnInit() {
    const first = await this.getFirst();
    const second = await this.getSecond(first);
    const third = await this.getThird(second);
  }
Spectrum answered 16/2, 2022 at 4:12 Comment(0)
M
8

Observables are great for streams, eg: BehaviorSubject. But a single call for data (eg http.get()) you're probably better off making the service call itself async.

async getSomethingById(id: number): Promise<Something> {
    return await this.http.get<Something>(`api/things/${id}`).toPromise();
}

Then, you can simply call it like so:

async someFunc(): Promise {
    console.log(await getSomethingById(1));
}

RxJS is very powerful, but using it for a simple api call seems like extreme overkill. Even if you need to massage the retrieved data you can still make use of the RxJS operators inside the getSomethingById function and just return the final result.

The clear advantage to async/await is that it's clearer to read and you don't need to jump through hoops to chain calls.

Manse answered 28/4, 2021 at 15:13 Comment(3)
Even streams are better as promises. Use AsyncEnumerable and implement them as a generator function.Massengale
@justin, in C#? Sure. Can you do that in TS/JS?Manse
yeah absolutely. You can make async generator functions and they return AsyncEnumerables and you can do for await (const i of items()), etc. https://mcmap.net/q/225112/-typescript-async-generatorMassengale

© 2022 - 2024 — McMap. All rights reserved.