Friday 28 June 2019

Promises vs Observables

Reactive Extensions (I've only used them in JavaScript/TypeScript with rxjs) is a really powerful tool and the more one plays with them more aware one becomes. Their aim is making easy to work with push streams (sequences) of data (I had given a classification of streams types here). It gives you so much power when compared to basic/classic event listeners.

On the other side, when working with an asynchronous operation that returns just once (it can return a single value or a collection, but it returns it just once) returning a Promise and using async/await moves you into a world miles ahead of the classic callback paradigm used for continuing asynchronous operations.

From the above, it would seem clear that we'll use Observables or Promises + async + await depending on whether we have multiple data or single data. But then, you find that the Angular HttpClient returns an Observable rather than a Promise and it seems odd. Even worse, if understanding how the magic of async/await and Promises (or Tasks in .Net) was not an easy endeavor for you, it hurts hard to read people saying that Promises are no longer cool and only Observables rule... what the fuck!?

There are many articles comparing Promises and Observables, this one is particularly good. You'll find also many discussions about why even when dealing only with a single value Observables are supposed to be always superior to Promises, and you'll find many disidents, like me :-). Observables can be cancelled and they are lazy, that's why even for a single data operation like an http request, they are supposed to be superior. That's true and that's false. Let me explain.

For those situations where you could need to cancel the http request, or you want to set it but run it lazily, yes, returning and Observable is a better option, but how often do you need any of those 2 features for a http request? Honestly, I hardly can think of a single case where I've needed that... On the other side, even after becoming familiar with pipe, flatmap, of... I think the cleanliness of the code that you can write with async-await lives in a different galaxy than the one you write with Observables. Let's see an example.

Let's say I have 2 async operations that I want to run sequentially, passing the result of the first one to the second one. Each operation would be like a http request, but in order to be able to run this code without depending on a server I'm just doing a sort of simulation.


function getUserId(userName:string, callback: (id:number) => void){
 let users: { [key: string]: number } = {
  anonymous: -1,
  francois: 1,
  chloe: 2
 };
 setTimeout(() => callback(users[userName] as number), 2000);
}

function getPermissions(id:number, callback: (permissions:string[]) => void){
 let permissions: { [key: number]: string[] } = {
  1: ["r", "w"],
  2: ["r"]
 };
 setTimeout(() => callback(permissions[id]), 2000);
}

Promises + async + await

I wrap each operation in a Promise returning function:

function promisifiedGetUserId(userName:string): Promise{
 return new Promise(res => getUserId(userName, res));
}

function promisifiedGetPermissions(id:number): Promise{
 return new Promise(res => getPermissions(id, res));
}

And now I can use them like this:

async function promisesTest(userName:string){
 let id = await promisifiedGetUserId(userName);
 console.log("id: " + id);

 let permissions: string[];
 if (id === -1){
  permissions = [];
 }
 else{
  permissions =  await promisifiedGetPermissions(id);
 }
 
 console.log("permissions: " + permissions.join(","));
} 

//sort of C#'s "async main"
(async () => {
 await promisesTest("francois");
 await promisesTest("anonymous");
})();

Observables

I wrap each operation in an Observable returning function:

function observableGetUserId(userName:string): Observable{
 return new Observable(observer => getUserId(userName, id => observer.next(id)));
}

function observableGetPermissions(id:number): Observable{
 return new Observable(observer => getPermissions(id, permissions => observer.next(permissions)));
}

And now I can use them like this:

function observablesTest(userName:string){
 observableGetUserId(userName).pipe(
  flatMap(id => {
   console.log("id: " + id);
   if (id === -1){
    return of([]);
   }
   else{
    return observableGetPermissions(id);
   }
  })
 ).subscribe(permissions => console.log("permissions: " + permissions.join(",")));
}

observablesTest("francois");
//observablesTest("anonymous");
 

I think the first code is way more clear and natural, don't you think? So Promises, await and async are alive and well! I've uploaded the code into a gist.

This said, I think the decision done by Angular of returning an Observable with their http client is OK. Converting that Observable to a Promise is as simple as invoking myObservable.toPromise(), so you can still do good use of async/await. At the same time, if you are in one of those cases where canceability and laziness are useful to use, that Observable is what you need, so in the end Angular is giving you the most versatile solution.

No comments:

Post a Comment