JavaScript Promises - Introduction

Chapter 21 21 mins

Learning outcomes:

  1. Asynchronous programming
  2. Limitations of callbacks
  3. The history of promises in JavaScript
  4. What are promises
  5. Benefits of using promises

Asynchronous programming

For a very long time since the advent of JavaScript (in 1995), there wasn't really ever much need to think about handling complex operations in it.

Both the language and the environment where it was meant to be used, i.e. the browser, were deemed to be pretty simple and easy to use, at least in the beginning.

But as applications started to use more and more JavaScript, more and more time-consuming operations began to be done in it. In this regard, the language acquired a large collection of APIs to facilitate these time-consuming operations.

Since JavaScript was single-threaded from day one, these time-consuming operations were carried out asynchronously, away from the main thread in order to prevent it from being blocked while the operations were under execution.

Initially, the two customary approaches to working with asynchronous operations were:

  1. Events — actions occurring on a web page.
  2. Callbacks — functions stored somewhere, to be called back later on.

Depending on the asynchronous operation at hand, it was either backed by an event-based system or by a callback-based system.

For example, the XMLHttpRequest API, used to dispatch HTTP requests in the background, was completely based on an event model, with such events as readystatechange, load, error, abort, and so on.

Similarly, the setTimeout() function, used to set up timers in the background, was based on a callback approach.

In the case of an event-based approach, the asynchronous operation would happen in the background and then dispatch events to be ultimately handled in the main thread.

In the case of callbacks, the asynchronous operation would once again happen in the background, but this time directly invoke a callback as soon as it reaches completion (which might represent a success or a failure).

Often times, APIs based on events were wrapped up inside reusable functions and then those functions would accept callbacks to be executed upon the dispatch of given events. In effect, this would make the underlying async operation as if it was based on a callback approach.

As an example, shown below is a reusable getRequest() function to dispatch an HTTP GET request to a given URL and then work with its response text:

function getRequest(url, callback) {
   var xhr = new XMLHttpRequest();
   xhr.onreadystatechange = function(e) {
      if (this.state === 4 && this.status === 200) {
         callback(this.responseText);
      }
   }
   xhr.open('GET', url, false);
   xhr.send();
}

The function uses the XMLHttpRequest API of JavaScript, which is based on an event model to handle working with network requests in the background, while being driven by a callback-based approach.

That is, we provide a callback to the getRequest() function (in its second argument) which is executed when a response is successfully received for a given HTTP request via the readystatechange event (and a couple other checks).

Events and callbacks in JavaScript together are extremely powerful and capable. They can be used to build superbly complex systems.

But this isn't without some limitations.

The limitations of callbacks

Before we dive into exploring the limitations of handling asynchronous operations naively using events and callbacks, it's worthwhile to group these two ideas under one unified concept of callbacks.

This is possible because event handlers are also merely callbacks (passed into addEventListener() or assigned to event-handler properties, such as onclick).

With this unification done, from this point onwards, we'll be using only the concepts of 'callbacks' in our discussion to refer to callbacks and event handlers.

To start with, note that there isn't anything wrong with callbacks in themselves. That is, the issue with using callbacks doesn't lie in how they work under the hood but rather in how they are written in source code.

Programmers are intrinsically tied to source code and if something ain't easy to manage or work with in the source code, it can lead to some considerable amount of frustration and difficulties in the longer run. This is exactly the case with callbacks, especially once we start to implement seriously complex tasks using them alone.

So what's the issue with writing code involving callbacks?

It's quite common to have scenarios in web applications whereby we need to execute an asynchronous task upon the completion of another asynchronous task. In other words, we need to nest async tasks.

Let's see a concrete example.

First, imagine we have a function getJSON() to make an HTTP GET request to a given URL endpoint and allow us to work with its response as a JSON object using a callback.

Here's the definition of the function:

function getJSON(url, callback, errorCallback) {
   var xhr = new XMLHttpRequest();
   xhr.onreadystatechange = function(e) {
      if (this.state === 4 && this.status === 200) {
         try {
            var json = JSON.parse(this.responseText);
            callback(json);
         }
         catch (e) {
            errorCallback(e);
         }
      }
   }
   xhr.open('GET', url, false);
   xhr.send();
}

As the response is received, the function parses the response text as JSON and then, on success, calls the callback() callback function. If, however, the JSON parsing phase produces any errors, a specialized error-handling callback, errorCallback() is invoked.

The function is pretty straightforward in its implementation, without rigorous error-handling capabilities, for e.g. dealing with HTTP 404 responses or cases where the network is down.

Now suppose we get the following JSON text returned for the URL endpoint /products/p1 that contains the following JSON, describing a product in an online course directory, with the ID p1:

/products/p1
{
   "gallery": "/gallery/p1",
   "name": "Learn JavaScript",
   "price": 2.5
}

Notice the gallery property — it contains a URL pointing to a collection of the URLs of all the images of the course.

In order to obtain the URL of all the images, we obviously need to make a GET request to this gallery URL and work with the response JSON.

Suppose that this /gallery/p1 endpoint returns the following JSON:

/gallery/p1
[
   "/static/images/i1.png",
   "/static/images/i2.png",
   "/static/images/i3.png"
]

Each item in the array is a URL pointing to an image.

Now, if we were to write a piece of code in terms of getJSON() to get a course's info and then of all of its images, we'd get something similar to the following:

function getJSON(url, callback, errorCallback) { /* ... */ }

getJSON('/products/p1', function(product) {
   getJSON(product.gallery, function(gallery) {}
      // Process images.
   );
});

Notice the increase in the indentation levels at each stage. For now, it might not seem like an issue but if our nested calls were more than what we have above, we'd run into a particularly ugly kind of code.

A general form is shown below:

asyncTask1(function(result1) {
   asyncTask2(function(result2) {
      asyncTask3(function(result3) {
         asyncTask4(function(result4) {
            asyncTask5(function(result5) {
               ...
            });
         });
      });
   });
});

An async task is done, then its result is used to execute a second async task, then its result is used to execute a third async task, and so on.

What we have above is called the callback hell, or the pyramid of doom.

Each subsequent async task requires us to push its callback code one level further to the right.

As the number of nested tasks increases, the code starts to go farther and farther to the right, leading to poorly structured code and, thus, to significant visual strain.

It's called the 'pyramid of doom' because the indentations literally form an approximation of a pyramid.

At this stage, if you're thinking that this is exaggerated, well, there have been production-level application code having such kind of a structure in the past! It's surely not something rare to see.

While there are ways to mitigate this, such as by replacing each anonymous callback with a named callback, where the named callback is a separate function, it still doesn't overall solve this lurking issue of easily managing nested async tasks.

Keep in mind that we didn't yet begin the argument on error-handling in such code.

Error-handling and debugging both are spectacularly difficult in such intricately nested async code. There is a much higher chance of errors, and in general, maintaining such code is a nightmare more than anything else.

If you were asked to devise an API to expose easier functionality to work with async code, what would you devise?

Well, some ingenious programmers already devised a commendable solution for this a long while ago, so fortunately we don't have to worry about it. Good, isn't it?

The solution is referred to as promises in JavaScript parlance.

Before we see what exactly are promises, let's spend some time in appreciating how we got to where we currently are with promises in JavaScript.

The history of promises

As stated above, following the increasing trouble faced by the JavaScript developer community in regards to handling async code purely using callbacks, people started to design potential solutions.

Since already, similar issues had been solved in other programming languages, people borrowed ideas and terminology from them into JavaScript.

Dojo Toolkit deferreds

Amongst the first of these solutions was one from Dojo Toolkit, one of the most popular and influential JavaScript libraries of its era.

Referred to as deferreds, the idea was to provide an interface to work with async code and then deal with the result of that async code using methods of the interface.

Promises/A specification

To kind-of standardize some of these earlier developments came in the Promises/A spec by the CommonJS community in 2010.

It formally introduced some terminology surrounding promises (or deferreds in other libraries), and the interface of a promise object — in particular, the then() method of a promise object.

The Q library

Following the Promises/A specification, the Q library was released in February 2010, implementing promises based on this specification.

The Q library became quite popular in a short amount of time, paving the way for even further developments to improve the ecosystem in place for handling async code in JavaScript.

jQuery deferreds

Later, at the start of 2011, the widely popular jQuery library released version 1.5 and, with it, the idea of deferreds. Based on the Promises/A spec, deferreds in jQuery were also meant to simplify the process of working with async code.

Due to the already well-established user base of jQuery, this idea of deferreds, and the general idea of promises (from the Promises/A spec that backed it), gained considerable traction.

Promises/A+ specification

Without a doubt, the Promises/A spec was a significant development in the evolution of promises in JavaScript but it was understated and had a couple of areas of improvement.

Consequently, in 2013, another specification improving upon Promises/A was drafted — it was called the Promises/A+ specification.

The name 'Promise/A+' clearly indicates that this specification is an improvement of the 'Promises/A' specification, by virtue of the '+' symbol.

The idea behind all these advancements was straightforward.

They all tried to transition callback-based, increased-indentation-level code into same-indentation-level code through the use of intermediary objects, placeholding the outcome of given async operations. (This concept of an intermediary object will become clear once you learn what are promises in the next section.)

ECMAScript 2015 promises

Now, seeing all these developments, and the innate need of a better system to handle async code in JavaScript natively, the ECMAScript 2015 standard formally introduced the notion of promises into the language.

Powered by the Promise interface, this system of promises in ECMAScript 2015 was compliant with the Promises/A+ specification.

And this is how promises evolved and eventually found their place in modern-day JavaScript.

What are promises?

It's now time to understand what 'exactly' are promises.

Defining it in terms of the idea behind it:

A promise is a means of simplifying the task of writing complex asynchronous code.

But this ain't the definition that precisely tells us what a promise is — it's, more or less, just a convenient way to look at it.

In more technical terms:

A promise is an object that represents the success or failure of a given operation, usually an asynchronous operation.

OK, now this might be too much to digest at a time!

Let's go slow.

In layman terms, a promise is simply a means of placeholding the success or failure of a given task. Usually, this task is an asynchronous task — what promises are really meant for.

'Placeholding' means that once the promise executes its given task, it serves to hold onto the result which can be a success or a failure.

Promises aren't meant for synchronous tasks but they can be used for handling them as well.

Using this held-on outcome, the program can then carry out respective actions on the outcome, such as further processing the data in the case of a success, or logging an error in the case of a failure.

A promise is basically just an intermediary object representing an async task. If a consequent action is desired, following the completion of that async task, then it's performed on this intermediary object.

One obvious benefit of this approach is that we can pass the object around a program, with different segments handling the eventual resolution of the underlying async task in different ways.

Don't worry if you couldn't understand a word in this discussion. Promises are verily one of the concepts in JavaScript that don't make any sense at all unless and until you rigorously experiment with them.

If these definitions go above your head, which they normally should, just ignore them for now. As this unit progresses, hopefully you'll master the craft of promises and soon be writing definitions on your own. Believe it!

In JavaScript, all promise-related utilities reside under the Promise interface.

Talking about how to actually create a promise and then work with it, we will discuss the bits and pieces to this in detail in the next JavaScript Promises — Basics chapter.

Benefits of using promises

Let's now get a quick overview of the benefits promises have to offer us.

First of all, promises mitigate the extra levels of indentation we saw earlier, by a mechanism for attaching callbacks instead of passing them to another function.

Secondly, error-handling in promises is a lot more concise and maintainable than error-handling in callbacks. Promises are built upon the conventional try..catch model used to respond to thrown exceptions and, thus, offers more convenience to developers in writing exception-handling code.

Moreover, the promise syntax relates to English language very closely, consequently making linked asynchronous calls seem way more meaningful and comprehensive to understand. At the heart of this idea lies the then() method, as we shall see in the next chapter.

And, by far, the biggest benefit of promises lies in the usage of async/await to make asynchronous code look as if it's synchronous code.

And the story doesn't even end here — it's just that we'll get a bit overwhelmed seeing all the minute pros that promises have to offer us, in one go.

It's best to keep things square for now and leave the rest of the ideas to be explored one-by-one in the upcoming chapters.