8 min read

This article by Rami Sarieddine, the author of the book JavaScript Promises Essentials, introduces JavaScript promises and reasons why should you care about promises when comparing it to the common way of doing things asynchronously.

(For more resources related to this topic, see here.)

Why should I care about promises?

What do promises have to do with all of this? Well, let’s start by defining promises.

“A promise represents the eventual result of an asynchronous operation.”

– Promises/A+ specification, http://promisesaplus.com/

So a promise object represents a value that may not be available yet, but will be resolved at some point in the future.

Promises have states and at any point in time, can be in one of the following:

  • Pending: The promise’s value is not yet determined and its state may transition to either fulfilled or rejected.
  • Fulfilled: The promise was fulfilled with success and now has a value that must not change. Additionally, it must not transition to any other state from the fulfilled state.
  • Rejected: The promise is returned from a failed operation and must have a reason for failure. This reason must not change and the promise must not transition to any other state from this state.

A promise may only move from the pending state to the fulfilled state or from the pending state to the rejected state. However, once a promise is either fulfilled or rejected, it must not transition to any other state and its value cannot change because it is immutable.

The immutable characteristic of promises is super important. It helps evade undesired side-effects from listeners, which can cause unexpected changes in behavior, and in turn allows promises to be passed to other functions without affecting the caller function.

From an API perspective, a promise is defined as an object that has a function as the value for the property then. The promise object has a primary then method that returns a new promise object. Its syntax will look like the following:

then(onFulfilled, onRejected);

The following two arguments are basically callback functions that will be called for completion of a promise:

  • onFulfilled: This argument is called when a promise is fulfilled
  • onRejected: This argument is called when a promise has failed

Bear in mind that both the arguments are optional. Moreover, non-function values for the arguments will be ignored, so it might be a good practice to always check whether the arguments passed are functions before executing them.

It is worth noting that when you research promises, you might come across two definitions/specs: one based on Promises/A+ and an older one based on Promises/A by CommonJS.

The new promise returned by the then method is resolved when the given onFulfilled or onRejected callback is completed. The implementation reflects a very simple concept: when a promise is fulfilled, it has a value, and when it is rejected, it has a reason.

The following is a simple example of how to use a promise:

promise.then(function (value){
   var result = JSON.parse(data).value;
   }, function (reason) {
   alert(error.message);
});

The fact that the value returned from the callback handler is the fulfillment value for the returned promise allows promise operations to be chained together. Hence, we will have something like the following:

$.getJSON('example.json').then(JSON.parse).then(function(response) {
   alert("Hello There: ", response);
});

Well, you guessed it right! What the previous code sample does is chain the promise returned from the first then() call to the second then() call. Hence, the getJSON method will return a promise that contains the value of the JSON returned. Thus, we can call a then method on it, following which we will invoke another then call on the promise returned. This promise includes the value of JSON.parse. Eventually, we will take that value and display it in an alert.

Can’t I just use a callback?

Callbacks are simple! We pass a function, it gets invoked at some point in the future, and we get to do things asynchronously. Additionally, callbacks are lightweight since we need to add extra libraries. Using functions as higher-order objects is already built into the JavaScript programming language; hence, we do not require additional code to use it.

However, asynchronous programming in JavaScript can quickly become complicated if not dealt with care, especially callbacks. Callback functions tend to become difficult to maintain and debug when nested within long lines of code. Additionally, the use of anonymous inline functions in a callback can make reading the call stack very tedious. Also, when it comes to debugging, exceptions that are thrown back from within a deeply nested set of callbacks might not propagate properly up to the function that initiated the call within the chain, which makes it difficult to determine exactly where the error is located. Moreover, it is hard to structure a code that is based around callbacks as they roll out a messy code like a snowball. We will end up having something like the following code sample but on a much larger scale:

function readJSON(filename, callback) {
   fs.readFile(filename, function (err, result) {
       if (err) return callback(err);
       try {
           result = JSON.parse(result, function (err, result) {
               fun.readAsync(result, function (err, result) {
                   alert("I'm inside this loop now");
                   });
                alert("I'm here now");
               });
           } catch (ex) {
       return callback(ex);
       }
   callback(null, result);
   });
}

The sample code in the previous example is an excerpt of a deeply nested code that is sometimes referred to as the pyramid of doom. Such a code, when it grows, will make it a daunting task to read through, structure, maintain, and debug.

Promises, on the other hand, provide an abstraction to manage interactions with asynchronous APIs and present a more managed approach towards asynchronous programming in JavaScript when compared to the use of callbacks and event handlers. We can think of promises as more of a pattern for asynchronous programming.

Simply put, the promises pattern will allow the asynchronous programming to move from the continuation-passing style that is widespread to one where the functions we call return a value, called a promise that will represent the eventual results of that particular operation.

It allows you to go from:

call1(function (value1) {
   call2(value1, function(value2) {
       call3(value2, function(value3) {
           call4(value3, function(value4) {
               // execute some code
           });
       });
   });
});

To:

Promise.asynCall(promisedStep1)
.then(promisedStep2)
.then(promisedStep3)
.then(promisedStep4)
.then(function (value4) {
   // execute some code
});

If we list the properties that make promises easier to work with, they will be as follows:

  • It is easier to read as in cleaner method signatures
  • It allows us to attach more than one callback to a single promise
  • It allows for values and errors to be passed along, and bubble up to the caller function
  • It allows for chaining of promises

What we can observe is that promises bring functional composition to synchronous capabilities by returning values, and error bubbling by throwing exceptions to the asynchronous functions. These are capabilities that we take for granted in the synchronous world.

The following sample (dummy) code shows the difference between using callbacks to compose asynchronous functions communicating with each other and promises to do the same.

The following is an example with callbacks:

   $("#testInpt").click(function () {
       firstCallBack(function (param) {
           getValues(param, function (result) {
               alert(result);
           });
       });
   });

The following is a code example that converts the previous callback functions to promise-returning functions that can be chained to each other:

   $("#testInpt").clickPromise() // promise-returning function
   .then(firstCallBack)
   .then(getValues)
   .then(alert);

As we have seen, the flat chains that promises provide allow us to have code that is easier to read and eventually easier to maintain when compared to the traditional callback approach.

Summary

Promises are a pattern that allows for a standardized approach in asynchronous programming, which enables developers to write asynchronous code that is more readable and maintainable.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here