3

Up until now, I've been creating functions that return directly from HTTP get, and then calling .subscribe on the return value. However, recently I've learned about Promise.all, and would like to know how I can use it to wait for both HTTP gets to be completed.

Example of how I'm currently doing it:

function getCustomerList() {
 return this.http.get("http://myapi.com/getCustomerList");
}

function otherFunction() {
  this.getCustomerList().subscribe() {
    data => {}, err => {}, () => {}
  }
}

This seems to work OK, but I keep wondering if I could do something like this instead:

function getCustomerList() {
  return this.http.get("http://myapi.com/getCustomerList").subscribe( data => {}, err => {}, () => {});
}

function getVendorList() {
  return this.http.get("http://myapi.com/getVendorList").subscribe( data => {}, err => {}, () => {});
}

function getAllInfo() {
  var p1 = getCustomerList();
  var p2 = getVendorList();

  Promise.All([p1, p2]);
  console.log("This should not run until both have returned");
}

But it always runs right away. So I tried to use .toPromise() in the get() functions with .then(), and it did the same thing. The only thing I haven't tried is maybe putting a .toPromise() on the outer function.

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Dan Chase
  • 993
  • 7
  • 18
  • 1
    It's unclear what specifically you've tried with `toPromise`, but note that RxJS has various operators that can run observables in parallel. Also, whichever you use, you aren't going to see `console.log("This should not run until both have returned");` wait until afterwards unless: 1. you put it in the callback; or 2. you are in an `async` function and have a Promise to `await`. See the canonical https://stackoverflow.com/q/14220321/3001761. – jonrsharpe Nov 18 '19 at 20:08

4 Answers4

4

The reason they ran right away is because you returned the subscription. You could do this:

function getCustomerList() {
  return this.http.get("http://myapi.com/getCustomerList").toPromise();
}

function getVendorList() {
  return this.http.get("http://myapi.com/getVendorList").toPromise();
}

function getAllInfo() {
  const p1 = getCustomerList();
  const p2 = getVendorList();

  Promise.all([p1, p2]).then(values => {
    console.log("This should not run until both have returned", values);
  });
}

But even better, you could use forkJoin:

import { forkJoin } from 'rxjs';

// component declaration and constructor omitted

customerList$ = this.http.get("http://myapi.com/getCustomerList");
vendorList$ = this.http.get("http://myapi.com/getVendorList");

getAll() {
  forkJoin(this.customerList$, this.vendorList$).subscribe(result => {
    console.log(result);
    // [{ customerList: ... }, {vendorList: ... }];
  });
}
inorganik
  • 24,255
  • 17
  • 90
  • 114
  • The first code block is missing the `.toPromise()` calls to convert the observables into promises. – GregL Nov 19 '19 at 17:20
2

when using .toPromise() you no longer have to subscribe

return this.http.get("http://myapi.com/getCustomerList").toPromise();

also your log won't wait for the promises to complete. You'll either have to log in a .then() function.

Promise.All([p1, p2])
  .then(t => console.log("This should not run until both have returned"));

or mark getAllInfo as async and await the Promise.all

await Promise.All([p1, p2]);
console.log("This should not run until both have returned");

I'd recommend looking more into async/await as well and not so much into observables. Just always convert them using .toPromise()

Matt
  • 2,096
  • 14
  • 20
1

You are using not Promises in the Promise.all, but Observables. You need to promisify them using .toPromise(). Then you need to continue your logic after fulfillment of the promise.

function getCustomerList() {
  return this.http.get("http://myapi.com/getCustomerList").toPromise();
}

function getVendorList() {
  return this.http.get("http://myapi.com/getVendorList").toPromise();
}


function getAllInfo() {
  var p1 = getCustomerList();
  var p2 = getVendorList();

  Promise.All([p1, p2]).then(_ => console.log("This should not run until both have returned"));  
}

As @jonrsharpe mentioned in the comments, it is not logically not wrap observable into promise for getting parallel work. You can use various of RxJS operators like combineLatest or zip to make wait for two observable to finish their work.

Suren Srapyan
  • 66,568
  • 14
  • 114
  • 112
1

Observables are lazier than promises

The first thing you have to realize is that observables are lazy, meaning they don't do anything (in this case, make the request) until you subscribe to them.

Promises, on the other hand, are eager, meaning they start doing work straight away, whether you do anything with the promise or not.

So yes, by using promises, you will make those requests straight away.

You could use Promise.all() if you want to

Here is how you would be best to structure your code to do what you want:

function getCustomerList(): Observable<CustomerList> {
  return this.http.get("http://myapi.com/getCustomerList");
}

function getVendorList(): Observable<VendorList> {
  return this.http.get("http://myapi.com/getVendorList");
}

async function getAllInfo() {
  var p1 = getCustomerList();
  var p2 = getVendorList();

  await Promise.all([
    p1.toPromise(),
    p2.toPromise(),
  ]);
  console.log("This should not run until both have returned");
}

By using async/await and converting to promises just for the call to Promise.all(), you will achieve exactly what you want to do.

But there is a better way without using promises

Since the Http service returns observables, which are kind of like super-promises, you can just use an observable operator to combine the two.

The only downside is you won't be able to use async/await with it.

Simply use combineLatest() to combine the two return values as an array, just like with Promise.all().

function getCustomerList() {
  return this.http.get("http://myapi.com/getCustomerList");
}

function getVendorList() {
  return this.http.get("http://myapi.com/getVendorList");
}

function getAllInfo() {
  var p1 = getCustomerList();
  var p2 = getVendorList();

  combineLatest(p1, p2)
    .subscribe(([customerList, vendorList]) => {
      console.log("This should not run until both have returned");
    });
}

Or you could return the result of combineLatest(), rather than subscribing to it directly.

GregL
  • 37,147
  • 8
  • 62
  • 67
  • If I want to assign variables from the results, for example this.customerList = data; from one, then this.vendorList = data; from the other, will this still work within each function, or do I have to do it later in getAllInfo? – Dan Chase Nov 18 '19 at 21:58
  • I would caution you against using variables to store values from observables, especially in Angular. You are better to have the view subscribe to the observable using the `async` pipe, as that will correctly manage subscriptions for you. It seems like you could benefit from learning more about how best to work with observables. There are plenty of good talks and documentation out there, you should check them out to learn more appropriate patterns. – GregL Nov 18 '19 at 22:05
  • much appreciate it believe me, but seldom do I just display it as-is, if you don't store it in a variable how do you use it? For example, in this case I'm actually pulling a template file, then another call reaches out to a CMS to pull an article, and I'm then combining the two into a result that is being shown on the page. I'm still unclear if I can just put the data => {} assignment in each function, or if I have to somehow sort them out after they both come back, in your example. – Dan Chase Nov 18 '19 at 22:11
  • 1
    If you want to perform some logic on the results, you can use pipeable operators like `map` to transform the result and still use async pipe in your template. To do that assign the observable in `getAllInfo()` to a component property and then async pipe it in the template – inorganik Nov 18 '19 at 22:34
  • @DanChase You can supply `combineLatest()` a _result selector_ function as the last argument that receives the result of each observable as parameters. In your case, that would be the template and the article contents. Then you can combine them inside that function, and return the combined result for use in your view (via the async pipe). – GregL Nov 19 '19 at 03:46
  • @inorgsaik and Greg Thank you both!! Your help is Most appreciated!!! – Dan Chase Nov 19 '19 at 15:37