Promises vs Observables – is there really a dilemma here?

This article assumes that the reader has had any either practical or theoretical dealings with both Promises and Observables. Anything will do, really. Being able to tell apart an integer from a double is a plus.

So what’s this all about, then?

Developing frontend apps usually implies the necessity of maintaining asynchronous communication with some sort of backend. Currently, this can be done via Promises and/or Observables.
But which ones should I use in my app? Maybe both? Is one ‘better’ than the other for me? In this post, we’ll try and have a look at both and decide which one is the thing we’re looking for – from an Angular developer’s perspective, although I’m sure any other framework users will discover direct analogies to their own situation.

A short refresher

Promises are quite straightforward things. An action is defined, and when it finishes – a callback function is called (if supplied). If any sort of error happens along the way – you can supply a callback for that as well. They are also always asynchronous and eager – they execute as soon as they’re instantiated. This is a very dry description, but essentially – that’s it for us.

Observables, on the other hand, are complex things with a number of versatile supporting functions. This is a good moment to mention that any functionality that can be achieved with Promises can be also achieved with Observables. That is because Promises are effectively Observables’ specific use-case. While Promises perform an action and then call a supplied callback, Observables can do that multiple times, acting as a value supplier that executes many times, giving the possibility of processing each of its returns by developer-defined observers. They can also be either synchronous or asynchronous – therefore they can also be used to maintain data flow in more complex structures regardless of whether it’s sync or async in nature. They are also lazy (they execute only when subscribed to), which within the realm of programming can be a tremendous advantage.

As such, the main difference is this: A promise is an async action with an option of a reaction to its result by a singular observer (async:1:1:1). An Observable is [N] sync/async actions, each potentially followed by a reaction [M] to each of their results by any number [O] of observers (sync/async:N:M:O). Therefore a Promise can be interpreted as an Observable performing exactly ONE async action [N], with the option to react to its single result [M] by a single [O] observer (async, N = 1, M = 1, O = 1, therefore sync/async:N:M:O results in async:1:1:1). Both can be used in various configurations – they can be chained, joined, forked, mapped, and so forth, using both predefined functions and/or our own custom code.

‘So basically, a rectangle/square type of relationship.’ Yup – at least from a behaviorally focused point of view. Some things are done differently, some have different names, but essentially that’s it – a Promise is a specific Observable.

OK, so why use Promises at all?

Having established the above, the central question of this post may be somewhat reformed – instead of ‘when to use Promises and when to use Observables’ it now effectively boils down to ‘when are Promises (not) enough’.

‘But wait wait wait – why would we want to use Promises at all? Observables got it all!’ This… is actually a viable way to go about this dilemma – and technically it’s correct. A Promise is just a subset functionality of the powerhouse that is an Observable. You can go about your business without ever touching a promise unless required by external code signatures.

To answer this question we could use the aid of a simple analogy, also from the realm of programming. Let’s take the following types: integer and double.

Integers are pretty simple things, but with a wide range of everyday programming applications. You can encounter them while dealing with loops, IDs of various kinds, indexing, arrays, and so on.
Double numbers are more versatile. They widen the available range of values by introducing fractions. Thus, they can represent many things (like currency) and have a lot of both business and programming applications.
Very much like Promises and Observables, anything done using an Integer can be achieved with Doubles. Why do we use Integers at all then? The answer comprises three key points: Efficiency, simplicity and readability.

Efficiency

Doubles are less memory efficient than Integers. They’re more robust, which reflects in such things as increased memory usage and calculation complexity. Therefore, for the sake of efficiency, we’ll default to using Integers where Doubles aren’t required. Where we stand nothing to gain by using Doubles, our main incentive for using Integers is that we don’t have any to use Doubles.

Simplicity

Doubles also usually come with additional functionality pertaining to how fractions are calculated, going beyond base-10 and such. However, if we don’t use any of it, any features that are required to handle more complex cases can only increase the calculations required to achieve something that could be a simple command involving Integers.
An example of something like that in the case of Observables would be the Subscription object returned after calling .subscribe on an Observable. If we can safely assume that the action runs only once and returns only one set of results at a single point in time, the Subscription becomes superfluous. It is a good practice to unsubscribe any subscriptions created when closing context (i.e. switching routes), but when the Observable is sure to execute only at one specific time and does not need to be told to ‘stop’ – like in the case of a simple http GET operation – the observable executes and finishes its lifecycle after returning a single result, so the only timeframe for canceling the subscription is between the Observable creation and the moment it finishes, which has very little practical application. This could theoretically handle the scenario in which the server takes too long to respond, but then we just end up reinventing timeouts, making it a very forced solution.

Readability (it’s mostly that, really)

Another important aspect of using an Observable or not is readability. Using an Integer over a Double supplies us with specific information regarding the variable’s behavior – it will never include fraction values. In the same fashion, the very choice of using a Promise also conveys information to the next person that will be working with our code. It indicates that the asynchronous action occurs once and once only, supplying a single result. If the same action was handled using an Observable, it would create the ambiguity of whether there are multiple underlying actions, resulting in a stream of return values, at various points in time. This, however, is not an iron-clad rule – this potential ‘confusion’ can be mitigated by using well-defined naming conventions, which in turn saves us the slight overhead caused by calling .toPromise() on every qualifying Observable supplied by external libraries. The choice of how to deal with this problem always depends on circumstance.

Promises VS Observables – the right tool for the job

The most common action for a frontend app that involves asynchronicity is a standard REST service call. It involves assembling a request (body, headers and all), sending it to the specified endpoint and waiting for the server to tell us how it went. This usually constitutes almost all of our web traffic while using our app.
For Angular, these are usually handled via the HttpClient, which returns Observables by default. These actions can be easily handled via Promises, but Observables provide us with advanced tooling – thanks to this it’s easier to handle more advanced scenarios like joining and forking streams of both asynchronously and synchronously retrieved data, or simply retrying our request without the need of custom code. Therefore the use of Observables in HttpClient serves the purpose of giving the developer options – whether or not they will be made use of depends on our circumstance.
In this case, the choice has been made for us – it’s Observables, and changing them with .toPromise() serves little purpose here, other than to maybe make things easier for developers with knowledge of only Promises – and the numbers of those are rapidly shrinking. If one wants to use Observables as he would use Promises, he needs only to use .subscribe as he would .then.

When the need to interact via a web socket arises, the answer is pretty straightforward – use an Observable. A socket most often communicates with its client more than once, over an extended period of time. Therefore, an Observable is a perfect receptacle – it can supply multiple values over time and won’t terminate until one of the parties (server/client) causes it to terminate – everything clicks.

‘Okay, so. Promises. When?’ Well, the answer isn’t as straightforward as I’d like it to be. Observables are widely considered as a successor to Promises. So why use Promises at all? This is where we need to think back to the chapter entitled with this very question.

What needs to be taken into account is the fact that both Promises and Observables are essentially utility wrappers for sync/async actions. Both can contain either or none of them – meaning an Observable can be composed of many interdependent Promises and emit their return values in the sequence. In turn, a Promise can contain many Observables internally, aggregating their values over time, only to emit the final result when it’s considered finished.
An example application of this could be gathering data from multiple sources in order to display a complex dataset – all the while maintaining the Promise’s simplistic interface. A black box that starts, executes, and returns a value to be handled by the developer – a wrapper to reduce the perceived complexity of the task to anyone using it, implying its overall behavior, and limiting possibilities of both confusion and misuse. That’s why using a Promise should be a deliberate design choice, where – for any reason – we want to prevent it from being handled like a full-fledged observable. Overall, it’s a good way to simplify the perception of complex tasks.

What it all boils down to

Let’s try to condense the main points of this article – and maybe create some new ones, based on what was already written.

When dealing with external libraries (i.e. Angular’s HttpClient), if an action is typed as an Observable, we shouldn’t ‘dumb it down’ to a Promise unless we’re absolutely sure that’s all it does. Otherwise, we risk limiting our own possibilities.

When defining our own actions, we should use a Promise if the action executes only once, and returns only one result set for sake of all the other devs that will have to work with our code. Similarly, if we decide that there is merit in exposing the additional functionalities provided by an Observable (like lazy execution, or multiple observers) despite its internal Promise-like nature, when using an Observable isn’t just a superfluous overcomplication, we should absolutely do that. Using Observables, an effective successor to Promises, is the current industry standard for a reason.

A good way to pick what we should use is to look at our functionality like it was a part of a library and choose what we ourselves would find useful in all prospective use cases. If the action was ‘blackboxed’ – is it better to be presented as an Observable, or maybe just a Promise?

With that I hope this article was able to do at least one of two things.

Using the right tool in critical code junctions can sometimes be as important as knowing why it was used, or what was our point in using them.

Author: Wojciech Kuroczycki

Skuteczne zarządzanie to klucz do sukcesu każdego przedsiębiorstwa. Ta oczywistość prędzej czy później da się we znaki podmiotom, które nie […]

02/09/2021

MVP. Co to znaczy i jakie przynosi korzyści?

Biznes
Ubezpieczenia
Bankowość

MVP to skrót od angielskiej nazwy Minimum Viable Product. Dosłowne tłumaczenie, czyli minimalnie wykonany (gotowy) produkt, choć brzmi trochę topornie, […]

Wprowadzenie nowego produktu na rynek nie jest prostym zadaniem, które potrafi pokonać nawet największych rynkowych graczy. Ciekawostka — słyszeliście kiedyś […]