Skip to content

Instantly share code, notes, and snippets.

@fmaylinch fmaylinch/reactivex.md
Last active Mar 21, 2018

Embed
What would you like to do?

From callbacks to promises to observable streams

In this article I’ll explain about the relationship between callbacks, promises and observable streams (AKA reactive programming).

Some of the examples in this document are written in this jsbin for you to try.

This document was written using the wonderful Markdown editor classeur.io.

Callbacks - sync and async

Callbacks are functions you pass to other functions. It's a very simple concept, but very powerful and somewhat tricky for beginners. Callbacks are used to tell a function to execute some piece of code (i.e. the callback) you send to them. Some functions execute the callback immediately (synchronously) and some functions execute after some time (asynchronously).

Functions that take other functions as arguments are called higher-order functions. One of the most popular higher-order functions is map. The map function takes a collection and a “transformation” function f. The result is a collection where each element of the original collection is transformed using f.

Let's see an example in Javascript, where map is a method included in the Array class:

function triple(x) {
  return x*3;
}

var array = [1,2,3];
var tripled = array.map(triple);
// tripled === [3,6,9]

From ECMAScript 6 we can also use lambda expressions in Javascript (called arrow functions):

var array = [1,2,3];
var tripled = array.map(x => x * 3);
// tripled === [3,6,9]

Note: I will use the lambda notation, since it's much shorter and easier to read. Most modern languages feature some sort of lambda syntax.

Async callbacks to return values later

The map function executes the callback synchronously: as soon as map is called, it iterates through the collection and executes the callback for each element, returning the resulting collection synchronously. Since map returns the result, you can assign it to a variable, chain another method call, etc.

Other functions execute the callback after some time. For example, if you make an HTTP request, the response may take a while, so the callback execution may be delayed. That's what happens with the jQuery.getJSON function. Let's see an example using the GitHub API:

// Let's browse the GitHub API for repositories about "reactivex"
var reposUrl = 'https://api.github.com/search/repositories?q=reactivex';
// The second argument of getJSON is the callback, called when the response arrives
jQuery.getJSON(reposUrl, repos => console.log('Found ' + repos.total_count));

jQuery.getJSON is one of those functions that take a callback to return some value later. This is one of the uses of callbacks, one that is very interesting for us now.

Usually functions return the result immediately (synchronously), so we can assign it to a variable:

var someVariable = someFunction();

It would be nice if you could call getJSON the same way:

// can we do this?
var respositories = jQuery.getJSON(reposUrl); 

Well, actually, you can do something like that too, if you use promises.

Promises: callbacks with extra features

It turns out that jQuery.getJSON returns something, and that's a promise. A promise is an object that contains (or will contain) some value inside. We can say that it "promises" to eventually give you the value you want.

jQuery.getJSON returns a promise of the repositories, so the last piece of code should be actually:

var reposPromise = jQuery.getJSON(reposUrl); 

How can we extract the promised repositories from there? Promises have some methods we can call on them, and in this case (jQuery promises) we can use the done method to extract the promised value:

var reposPromise = jQuery.getJSON(reposUrl);
reposPromise.done(repos => console.log('Found ' + repos.total_count));

Or if we inline the reposPromise variable:

jQuery.getJSON(reposUrl).done(repos => console.log('Found ' + repos.total_count));

Let's remember the callback version:

jQuery.getJSON(reposUrl, repos => console.log('Found ' + repos.total_count));

They're quite similar. But the promise version has some advantages:

  1. We can call getJSON like a "normal" method that returns something.
  2. We can take the promise and pass it anywhere, like it was the result (it's actually a promise of the result).
  3. Since getJSON returns an object (a promise), we can chain method calls on it.

The third advantage comes very handy when we have to chain different asynchronous calls.

Chaining promises and the callback hell

Let's say we want to get the repositories and then get the issues of the first repository found.

Using callbacks we would do:

var reposUrl = 'https://api.github.com/search/repositories?q=reactivex';

jQuery.getJSON(reposUrl, repos => {

  var repoName = repos.items[0].full_name;
  console.log('First repo found: ' + repoName);
  
  var issuesUrl = 'https://api.github.com/repos/' + repoName + '/issues';
  
  jQuery.getJSON(issuesUrl, issues => {
    console.log('Repo has ' + issues.length + ' issues');
  });
});

To make it more clear, let's write utility functions to get repos and issues:

function getRepos(query, callback) {
  var reposUrl = 'https://api.github.com/search/repositories?q=' + query;
  return jQuery.getJSON(reposUrl, callback);
}

function getIssues(repoName, callback) {
  var issuesUrl = 'https://api.github.com/repos/' + repoName + '/issues';
  return jQuery.getJSON(issuesUrl, callback);
}

// Note we "return" in both functions. That's because getJSON returns a promise (we will use them later).

Now our code with callbacks would be like this:

getRepos('reactivex', repos => {

  var repoName = repos.items[0].full_name;
  console.log('First repo found: ' + repoName);
  
  getIssues(repoName, issues => {
    console.log('Repo has ' + issues.length + ' issues');
  });
});

It's much clearer, but the "problem" here is that we have to nest callbacks. With 2 callbacks it's not so bad, but with 3 or more we would get into a callback hell.

Let's see how we would do it using promises:

getRepos('reactivex').done(repos => {

  var repoName = repos.items[0].full_name;
  console.log('First repo found: ' + repoName);
  
  getIssues(repoName).done(issues => {
    console.log('Repo has ' + issues.length + ' issues');
  });
});

Hum... We got a similar code. But wait, there's another method, called then, that is similar to done but with an interesting difference. In the callback you pass to then you may return a promise, and in that case the then method returns a promise where you can get the value from the inner promise. Seems complicated but an example is easier to understand:

Note: In the following snippet I extract the promise variables so you can see what I'm doing more clearly.

var reposPromise = getRepos('reactivex');

var issuesPromise = reposPromise.then(repos => {
  var repoName = repos.items[0].full_name;
  console.log('First repo found: ' + repoName);
  return getIssues(repoName); // this makes `then` return a promise with the issues
});

issuesPromise.then(issues => {
  console.log('Repo has ' + issues.length + ' issues');
});

Now the code doesn't have nested callbacks. It's a bit longer but we will shorten it in a minute, I promise. :)

Before that, one tricky thing I'd like to remark: notice that the return statement is inside the callback passed to then but, due to the way promises work, it will make the then call return a promise with the same value (you can think as if they were the same promise). That's why I assign:

 var issuesPromise = reposPromise.then(...);

Note: In jQuery promises you use done and then. In other languages or implementations you will find the same features but maybe using other methods.

Now, as I promised, I will inline the promise variables again, to shorten our piece of code:

getRepos('reactivex').then(repos => {
  var repoName = repos.items[0].full_name;
  console.log('First repo found: ' + repoName);
  return getIssues(repoName);
}).then(issues => {
  console.log('Repo has ' + issues.length + ' issues');
});

As you can see, using promises we got a piece of code that is not nested, and resembles how it would look if the calls weren't asynchronous (compare both versions):

// FAKE CODE, as if calls were synchronous
var repos = getRepos('reactivex');
var repoName = repos.items[0].full_name;
console.log('First repo found: ' + repoName);
var issues = getIssues(repoName);
console.log('Repo has ' + issues.length + ' issues');

Handling errors

Another cool feature of promises is the way we can handle errors in any async call.

Any of our HTTP requests may fail for several reasons, and we may want to do the same thing in any case -- for example let the user know that we couldn't connect to the server.

If we chain done/then calls, any error will be propagated through the chain, and we can handle it any time (usually at the end) using the fail method (it works as some sort of catch clause).

getRepos('reactivex').then(repos => {
  var repoName = repos.items[0].full_name;
  console.log('First repo found: ' + repoName);
  return getIssues(repoName);
}).then(issues => {
  console.log('Repo has ' + issues.length + ' issues');
}).fail(error => {
  console.log('Sorry, something went wrong: ' + JSON.stringify(error));
});

To check that the fail callback is called when something goes wrong you can wait for the GitHub API to go down... or quicker: you can force an error by just modifying any API urls (e.g. in getRepos or getIssues methods ).

Next we will see reactive streams or, more properly, observable streams. They are quite related to promises. If promises are like callbacks on steroids, observable streams are like promises on steroids.

But before that, a little explanation of how you could make promise objects yourself.

Make your promises and fulfil them... or not

jQuery.getJSON returns a promise. You could also write methods that return a promise you create yourself.

Using jQuery promises you create a promise the following way:

// First create a deferred object
var deferred = jQuery.Deferred();
// From it you get the promise
var promise = deferred.promise();

Note: jQuery uses a specific implementation of promises. I would recommend to read also about the promises included from ES6, which are the official promises in JavaScript. I will probably update this article using ES6 promises

Later, resolve the deferred object so the promise returns the value through the done or then methods.

deferred.resolve(10);

Or you could reject the promise so it returns an error through the fail method:

deferred.reject("some error occurred");

But don't call both resolve and reject; you should only call one of them. It makes no sense to say that the promise succeeded and failed.

When you create a deferred object from a function, you should return the promise, not the deferred. The caller will get the promise and wait for the result using the methods we've seen: done, then, fail.

Simple example: method that builds a promise

This method will be very simple and stupid, but the purpose is just to understand how you would code a method that creates and returns a promise (and resolves or rejects it), and how you would call that method.

Let's say we want to code a method that returns the inverse of a number we pass as argument.

// Returns a promise of 1/x
function invert(x) {

  var deferred = jQuery.Deferred();
  var promise = deferred.promise();

  if (x != 0) {
    deferred.resolve(1/x); // This will trigger the done() callback
  } else {
    deferred.reject("I can't divide by 0"); // This will trigger the fail() callback
  }
  
  return promise;
}

// Let's call the method
var x = 10;
invert(x)
  .done(y => console.log('1/' + x + ' is ' + y))
  .fail(error => console.log('Something went wrong: ' + error));

Observable streams

Note: This section is not completed.

Observable streams are an evolution of promises. They are asynchronous like a promise, but have some powerful extra features since they handle the result as a stream.

A stream is similar to a collection, an asynchronous collection. That means that the items in the stream may come at certain times (we don't have them all from the beginning). A stream is a flow of values (items, objects) that keep coming for some time until the stream completes gracefully or stops abruptly due to an error.

An observable stream takes the concept of a stream and joins it together with the observable pattern. This way, you may observe the stream and get notified every time a value comes. Actually, there are 3 different events you may be notified of:

  1. A new value comes
  2. The stream completed (reached the end)
  3. The stream failed (with some error)

The order of events is: first you receive some values (zero or more) and then you receive either the "completed" signal or the "error" signal. After the "completed" or "error" signal, no more events will come.

The happy scenario (hopefully) is: first you receive some values, then the "completed" signal.

Example with an array

To better understand observable streams, we can start with a known concept like an array and turn it into an observable stream. We'll use the RxJS library.

// Convert an array to an observable sequence
var array = [1,2,3,4,5];
var source = Rx.Observable.from(array);

// Now we can subscribe to (observe) the sequence 
source.subscribe(x => console.log("Received: " + x));

// We can listen to the 3 events (value, error, completed)
source.subscribe(
  x => log("Received: " + x),
  error => log("Error: " + error),
  () => log("Stream finished successfully")
);

We can see an observable stream from the inside by creating one. When you create an observable stream you decide what to do when someone subscribes: you decide when to send values, complete the stream or make the stream fail.

var source = Rx.Observable.create(function (observer) {
  // Yield some values and complete
  observer.onNext(1);
  observer.onNext(2);
  observer.onNext(3);
  observer.onCompleted();
});

source.subscribe(
  x => log("Received: " + x),
  error => log("Error: " + error),
  () => log("Stream finished successfully")
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.