Promises vs Observables for AngularJS-to-Angular migration

Oleksandr - May 15 '19 - - Dev Community

AngularJS(Angular 1) was using Promises heavily for HTTP calls, while Angular has network calls wrapped in Observables. This caused that some devs met specific issues while migrating projects from AngularJS to Angular. I want to address these issues here and describe why they can appear at all by reviewing typical differences between Observables and Promises.


Observables — more capabilities for web-devs. (pic by mediamodifier)

Pre-requisites: you should know JS Promises

Observables and Promises — short intro

At first glance — Observables are just advanced Promises: Promises emits one value and complete (resolve), Observables emit 0, one or many values and complete as well (emit and complete are different actions). For HTTP service in AngularJS and Angular provides only one value — so seems both frameworks work very similar in this case.

// Observables in Angular 2+
const sourse$ = this.httpServie.get('https://some_url.com')
source$.subscribe(
    (data) => handelData(data), // success handler
    (err) => handleError(err),  // error handler
    () => completeHandler() // onComplete handler
)

// Promises in AngularJS
const soursePromise = http$.get('https://some_url.com')
soursePromise.then(
    (data) => handelResolve(data), // resolve handler
    (err) => handleReject(err) // reject handler
)
Enter fullscreen mode Exit fullscreen mode

And one may think that it is enough just rename $http to this.httpService , then to subscribe and everyone will be happy. In very simple apps it even can work — but if you application is doing something more then ‘Hello world’ — plz pay attention to these differences.

#1 Eager vs Lazy

Take a look at example below:

//Promise-wrapped http request
saveChanges(data) {
  return $http.post('https://some_url.com', data)
}


//Observable-wrapped http request
saveChanges(data) {
  return this.httpService.post('https://some_url.com', data) // doesn't do request!
}
Enter fullscreen mode Exit fullscreen mode

When I call saveChanges method — the first example with Promise-wrapped request will work as expected. But in seconds Observable-wrapped example nothing will happen because Observables are lazy-evaluated while Promises are eager-evaluated.

This means that Promises doesn’t care whether they have some subscribers to get their result or not. But Observables (to be precise — cold Observable) will be cold only if we subscribe to them. In the case above you should subscribe to Observable returned by saveChanges function.

saveChanges(data).subscribe()
Enter fullscreen mode Exit fullscreen mode

To keep an eye on it — use rxjs-no-ignored-observable rule from rxjs-tslint-rules by Nicholas Jamieson.

#2 Promises cannot be canceled while Observables can be unsubscribed

Again, start with example when on input text change we do search on a back-end:

// html
<input ngKeyup="onKeyUp($event)">

//Promise-wrapped HTTP request
saveChanges(event) {
  const text = event.target.value;
  $http.get('https://some_url.com?search=' + text)
    .then((searchResult) => showSearchResult(searchResult))
}
Enter fullscreen mode Exit fullscreen mode

What is a drawback here — that you cannot reject results of the previous request if the user continues typing (debounce make this problem a bit less but doesn’t eliminate it). And one more issue — race condition is possible (when later request result will come back faster then earlier one — so we get incorrect response displayed).

Observable can avoid this concern quite elegant with switchMap operator:

// html template
<input id="search">

//Observable-wrapped HTTP request
inputElem = document.querySelector('#search');
search$ = fromEvent(inputElem, 'keyup');

ngOnInit() {

  search$.pipe( // each time new text value is emitted
    switchMap((event) => { // switchMap cancel previous request and send a new one
      const text = event.target.value;
      return this.httpService.get('https://some_url.com?search=' + text);
    })
  )
    .subscribe((newData) => this.applyNewData(newData))  // use new data
}
Enter fullscreen mode Exit fullscreen mode

Here we convert input text typing to observable values emissions. Each time new text value is emitted switchMap operator will cancel previous network request (if it is not finished yet) and send a new one.


Packtpub.com and I prepared a whole RxJS course with many other details of how you can solve your every-day developer’s tasks with this amazing library. It can be interesting for beginners but also contains advanced topics. Take a look!


#3 No built-in retry or repeat logic for Promises. ‘repeat and ‘retry operators for Observables.

You can implement retry logic with Promises but it looks a bit cumbersome:

var request = function() {
  $http({method: 'GET', url: path})
    .success(function(response) {
      results.resolve(response)
    })
    .error(function() {
      if (counter < MAX_REQUESTS) {
        request();
        counter++;
      } else {
        results.reject("Could not load after multiple tries");
      }
    });
};

request();
Enter fullscreen mode Exit fullscreen mode

While same code Observables will be much shorter:

this.httpService.get('https://some_url.com/data').pipe(
    retry(MAX_REQUESTS)
)
Enter fullscreen mode Exit fullscreen mode

Read more about repeat and retry operators use-cases in my article.

#4 A small number of Promises combination tools. Observables provide a wide variety of operators for that.

For Promises all the possibilities you can combine resulsts are:

Promise.all  — waiting for all Promises to be resolved and then provide array of results.

Promise.race  — wait till one of the Promises is resolved and return that result.


Observables provide very rich ammo for making combinations:

  • combineLatest(observable1, observable2,…) — waits for any of observable to emit and provide array of last emitted values from all observables (result: [value_obs1, value_obs2,..]). Very good if you should update page on a new data from a few different sources.

  • observable1.pipe(withLatestFrom(observable2) — on each value from observable1 also provide last emitted value for observable2 (result: [value_obs1, value_obs2]).

  • forkJoin(observable1, observable2,…)— analog for Promise.all — waits till all Observables are complete and then emits an array of last values from all of the argument observables.

  • zip (observable1, observable2,…)— waits for all of the argument observables to emit values with the same index and provide an array of emitted values with the same index (result: [value_obs1, value_obs2,..]).

  • race(observable1, observable2,…) — returns an Observable that mirrors the first source Observable to emit an item.

  • merge(observable1, observable2,…) — subscribes to every argument observable and re-emits values from all of them.

  • switchAll  — if previous Observable is not completed — cancel it and subscribe to new one.

  • concat(observable1, observable2,…) — start next Observable sequence only after previous one is done (emits values one by one after each specific Observable completion)

And many more (switchMap, mergeMap, partition, iif, groupBy, window, etc)

You can learn more about these operators here:

  1. Learn to combine RxJs sequences with super intuitive interactive diagrams
  2. Official docs with examples
  3. “Hands-on RxJS for Web Development” video-course.

#5 Easy to prevent race condition with Observables and hard - with Promises.

Say we periodically make a network request for updated data. But in some situations later request result will come back faster than earlier one — so we get incorrect (earlier) response displayed as last.

getData() {
  $http.get('https://some_url.com/data')
    .then((searchResult) => {
        doSomething(searchResult)
    }
  })
}

setTimeout(getData, 5000);
Enter fullscreen mode Exit fullscreen mode

This code can be possibly affected by the race condition issue.

To prevent this with Observable-wrapped requests we can use concatMap operator.

interval(5000).pipe(
    concatMap(() => this.httpService.get('https://some_url.com/data'))
)
.subscribe(doSomethingWithData)
Enter fullscreen mode Exit fullscreen mode

concatMap will make next network call only after previous is done and handled. Of course, if you don’t need previous results — then use switchMap (as in the first example of this article).

Conclusion

During migration from AngularJS (uses promises for network calls) to Angular (uses Observable) you should be aware of possible differences of Promises and Observable. Hope my article helped you to clarify this topic. Now it’s time to migrate!

Like this article? Let’s keep in touch on Twitter.

This post was originally published in ITNEXT.


Starting from section 4 of my RxJS video course advances staff is reviewed — so if you familiar with RxJS already — you can find something useful for you as well: higher-order observables, anti-patterns, schedulers, unit testing, etc! Give it a try !

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .