DEV Community

Cover image for Observables Or Promises
Kinanee Samson
Kinanee Samson

Posted on

Observables Or Promises

What comes to your mind when you think of handling asynchronous operations in JavaScript? Perhaps you still prefer using call back based code? Or as a modern developer you might be using super awesome Promises? If you are a 10x developer then you might be using Observables! In this article we will look at both data structures (Promises or Observables) briefly and which one of them we should be using in 2021.

Promises are one of the coolest features of working with JavaScript, they can help you reduce a lot of call back functions, throw in the await/async syntax and you are dealing with asynchronous code as if you were writing synchronous code and still reducing lines of code and multiple .then chaining.

Observables are an ergonomic way of dealing with streams of asynchronous event/data as they progress through time. Observables were introduced to JavaScript due to the lack of native support for multiple streams of asynchronous data/event in JavaScript.

Conditions where you had to use Promise.race or Promise.all due to the obvious fact that you have to manage multiple streams of asynchronous operations at the same time. Observables are more suited to situations like this, in fact they were built for situations like this. We will proceed to examining the how each data structure works and then try to see the drawbacks and advantages of using each of them.

Promises

Promises are data structures for handling asynchronous operations. Asynchronous operations are operations that we can start now and finish later. Promises were introduced to help reduce the complexity that came with call back based code, think call back hell.

To really understand how promises work we have to take a bite from the real life example of Promises. When you make a promise to someone, you are telling them that you will give them something later in the future, you might know when you will do that or you don't have an idea when you will. Likewise promises in JavaScript, you are not totally sure how long is going to take for the promises in your code to be resolved, or you might?

Following on from the analogy we presented earlier, when you make a promise to someone, you will either fulfil that promise or you won't. Same thing with JavaScript, a Promise is either fulfilled or not in which case it will be rejected. Promises in JavaScript can resolved (fulfilled) with the data or rejected with an error. Let's create a Promise so we can get a better overview of how they look like.

let myPromise = (num) => {
  return new Promise((resolve, reject) => {
    if (num > 0){
      setTimeout(100, resolve(num))
    } else {
      reject('Oops try a higher number')
    }
  })
}


let prom = myPromise(2)
let prom2 = myPromise(0)

prom.then(console.log) // 2

prom2.then(console.log).catch(err => console.log(err))
// Oops try a higher number

console.log('hey') // 'hey' will be printed first.
Enter fullscreen mode Exit fullscreen mode

In the above example we create a simple promise that will resolve if the number we pass in as an argument is greater than zero however it will fail (reject) if otherwise. If you paste in this code snippet and run it in your browser console, you will observe that hey is logged to the console before the values from the other promises, this is because a Promise is what is described as a PUSH SYSTEM. Observe how we deal with the actual data that is returned from the promise by calling the .then() method, and how we handle errors using the .catch method.

You will agree with me that this is a much smoother way of handling asynchronous operations than using call-back based code. Let us get a look what a PUSH SYSTEM is.

PUSH SYSTEMS

a push system in JavaScript is a data structure that pushes the data contained within it to the consumer. The consumer in this context is the environment that our JavaScript code is being executed in, it could be the browser if we working on frontend development projects, while on a backend project it would usually be the nodejs runtime.

The when we create an instance of a promise, we call a function that returns a promise the value contained within the promise if is not available to us, as demonstrated above. We have to call the then method to get access to the data inside the promise. You will have also observed that hey is logged out before the values from the promises are. This is because immediately we call console.log() the value returned from that function is consumed immediately, with promises the case is rather different.

It is up to the promise to decide when it's value is available to the consumer, this is a PUSH SYTEM. Ordinary functions like the console.log() are known as PULL SYSTEM because their valued are pulled out by the consumer immediately they are executed. When we create a promise, everything can be done and until the resolve method is called inside the promise, this allows the promise to return some data. There is also a cool method for handling errors. When we call the reject method inside the promise, we can provide some information about what went wrong.

GLITCHES WITH PROMISES

Promises are really nice data structures however there are some draw backs with using promises, some of which we will discuss below;

  • A single Promise cannot return more than a single value, you can only call resolve in a promise once, effectively knocking you out of sending values in the future, this is the biggest draw back of working with promises. There are times when we handle some operations that returns huge amounts and as such it would be convenient if we could send the data in chunks rather than one huge gum ball.

  • Handling multiple promises are not quite as convenient as I think it should be, you can use Promise.race() to await the first completed promise in a list or you could use Promise.all() to await all the promises in a list of promises. There are no custom built functions for manipulating Promises as you like, you are left with the task of building one for yourself.

  • A Promise can only return a value, when it's resolved or rejected and only that, you have to wait for your data to arrive first then you can start diving away at it, it could be nice if promises could format data and return it in a desired way and not having another code concern to worry about.

Enter Observables

Observables were built to solve all of the above problems that faced Promises, this should mean that Observables are quite awesome data structures, first thing you need to know is that JavaScript do not have built in support for Observables so you have to install the rxjs library form npm to use Observables. To do that run npm i rxjs and import it into your project.

Observables presents a way to handle asynchronous events as a stream of data that flows through time, at each point in time, the data could be in any state, we could make changes to the data without retrieving it's value, we could also format the data still without consuming it.

Let's create the simplest Observable to get a better picture of the situation;

import { Observable } from 'rxjs';

let $myObservable = new Observable(subscriber => {
  subscriber.next('simple Observable')
})

$myObservable.subscribe(console.log) // simple Observable
Enter fullscreen mode Exit fullscreen mode

When we create an Observable we are required to pass in a compulsory function that gives us access to a subscriber object that we can use to return values from that observable by calling the next method on it. We can call the next method as much as we like because an Observable can emit zero to infinite values.

let $myObservable = new Observable(subscriber => {
  subscriber.next('simple Observable')
  subscriber.next(200)
  subscriber.next({ name: 'sam' })
})

$myObservable.subscribe(console.log)
// simple Observable
// 200
// {name: 'sam'}
Enter fullscreen mode Exit fullscreen mode

Observables like promises also have a function for handling errors,

import { Observable } from 'rxjs';

let $myObservable = new Observable(subscriber => {
  subscriber.next(200)
  subscriber.error('Oops')
})

$myObservable.subscribe(
  v => console.log(v), // 200
  v => console.log(`some error ${v}`) // some error Oops
)
Enter fullscreen mode Exit fullscreen mode

An Observable can only be consumed by calling the subscribe method on the instance of the Observable which we are working with. The subscribe method is just a way to get access to values returned by the Observable. However the Observable we are subscribing to is not keeping track of how many times we decide to do that. It doesn't maintain a list of subscription calls. And when we call the subscribe method, we are not immediately consuming the value, because it might not be readily available, rather the subscribe method is just a switch for kick starting the execution of the Observable, when data or events from the computation of the Observable is ready it is then available for consumption, thus enabling Observables to behave both synchronously as we have seen above and also asynchronously as we will see below.

let $observable = new Observable(subscriber => {
  setTimeout(() => subscriber.next('I am asynchrous'), 200)
  subscriber.next('I am synchronous')
  subscriber.next('I am also synchronous')
})

$observable.subscribe((v) => console.log)
// I am synchronous
// I am also asynchronous
// I am asynchronous

Enter fullscreen mode Exit fullscreen mode

If it not already apparent, Observables are also PUSH SYSTEMS, they share the same philosophy with promises by pushing up their values to the consumer when it is available, instead of when they are are executed. The major difference between Promises and Observables is the ability of Observables to push up to infinite amount of values or events over time, rather than just a single value.

You will have seen that we can call the next method on the subscriber with a value it delivers the value when the Observable is subscribed to, if is available then. If there is an API that returns asynchronous data or event which is to be emitted by the next method. The Observable with proceed to emitting other values or events that are readily available, until the result of the data from the asynchronous process is available.

An Observable will continue to emit values or events if it is available, until we call the complete method on the subscriber, this wraps of the execution of the observable, all further calls to subscriber.next() is ignored because the Observable is done emitting values. We also saw how we could use subscriber.error() to handle errors.

import { Observable } from 'rxjs';

const $observable = new Observable((subscriber) => {
  subscriber.next('I will execute');
  subscriber.complete();
  subscriber.next('i wont execute');
});

$observable.subscribe((v) => console.log(v));
// I will execute
Enter fullscreen mode Exit fullscreen mode

All further subscriptions to the Observable will adhere to the complete function and will be marked as done when the complete function is called.

Pros Of Observables

Observables are really good because the rxjs ships with a bunch of functions that simplify the creation and manipulation of Observables, most of the use case has already been accounted for so you would not need to create your own custom Observable. However if you are working on a mega project and you need your own custom Operator the rxjs library allows you to create one. We will look at some of the built in operators that ships with the library to get an idea of how we could be using Observables.

import { from, of } from 'rxjs';

let $observable = from([1, 2, 3, 4, 5]);
let $observable2 = of({ name: 'John Doe' });

$observable.subscribe(console.log); // 1, 2, 3, 4, 5
$observable2.subscribe(console.log) // { name: 'John Doe' }
Enter fullscreen mode Exit fullscreen mode

The two examples we just saw above are use cases of the operators that rxjs comes loaded with, the two above are categorized as creation operators, this is because they let us create new Observables based of some values. We also have pipeable operators that allows us to manipulate data from an observable and return another observable off it.

import {  from, filter, map, find } from 'rxjs';

let $observable = from([1, 2, 3, 4, 5]);

let filteredObservable = $observable.pipe(
  filter(x => x%2 == 0) // find all even numbers
)
let mappedObservable = $observable.pipe(
  map(x => Math.pow(x, 2)) // raise all numbers to the square of 2
)

let foundObservable = $observable.pipe(
  find( x => x===2) // find and return the value equal to 2
)

filteredObservable.subscribe(console.log) // 2, 4
mappedObservable.subscribe(console.log) // 1, 4, 9, 16, 25
foundObservable.subscribe(console.log) // 2
Enter fullscreen mode Exit fullscreen mode

We are already seeing another advantage of working with Observables, it gets us to write simpler and shorter functions, and it might be a game changer for you if you prefer to write functional code. The pipe function we saw above allows us to stack multiple pipeable operators on top of each other. We could write an observable that sits around and spits out the number of seconds elapsed after each second, we could allow that Observable to continue emitting values taking all the even numbers until we get to then?? Let's try.

import {
  filter,
  interval,
} from 'rxjs';
import { takeWhile } from 'rxjs/operators';

const $interval = interval(1000);

$interval
  .pipe(
    filter((x) => x % 2 == 0),
    takeWhile((x) => x < 12)
  )
  .subscribe(console.log); // 0, 2, 4, 6, 8, 10
Enter fullscreen mode Exit fullscreen mode

See how short and concise this code is? We are obviously doing a lot with a little, if we wanted to use vanilla JS to accomplish this, we would certainly have to write a lot more code than we did.

So you have a code base you are working on and you are using promises to handle asynchronous tasks, you might be asking how do I just make the change without having to sweat it? Don't sweat it because Observables have built in support for Promises, you can easily convert a Promise to an Observable and vice-versa.

import { from } from 'rxjs';

let myProm = new Promise((resolve, reject) => {
  resolve(2);
});

let promToObservable = from(myProm);

promToObservable.subscribe((x) => console.log(`the value is ${x}`));
// the value of x is 2
Enter fullscreen mode Exit fullscreen mode

Cons Of Observables

The only draw back with using Observable is the small learning curve associated with getting familiar with the vast amount of operators, but you could always read the official documentation. You should really consider using Observables in your code base, in typescript, an Observable can be strongly typed to emit a particular type of value.

Sometimes using Observables is just an overkill because the situation does not need that level of complexity. So you'd rather just use simpler methods.

What do you think? which approach do you think is better? I would love to hear your opinion below.

Discussion (0)