Javascript Promises Demystified

If you ever had to write a complex application (and I use the word complex very loosely here), you've most likely had to run asynchronous operations. A couple of years ago, the de-facto method would have been to use callbacks for asynchronous operations. Although that works, the problem with callbacks is that your code becomes a mess very quickly, is hard to maintain, debug, and understand. Sure, you can break down everything into smaller functions and just pass a function to your callback, but what about all those nested callbacks? Nasty. The popular term for this is callback hell.

Enter Promises.

What Are Promises?

A promise is the eventual result of a function - either successful or not. A function returns a promise object, and will resolve it if successful or reject it if an error occurs.

Based on this definition, we now understand that a promise can be in three different states:

  • A pending state: the result has not yet been determined and we are waiting for the asynchronous operation to complete.
  • A resolved state: the asynchronous operation completed successfully.
  • A rejected state: something went wrong during the asynchronous operation (we will get to the error handling part shortly).

Getting Started

Promises have a then method that you can use to get the result, which as we learned previously, can either be resolved or rejected. The then method takes two arguments: the success callback, and the error callback.

Using callbacks, we would write something similar to this:

formatVisit(visit, function (formattedVisit) {
    console.log(formattedVisit.address);
});

But with promises, the same code can be written as follows:

formatVisit(visit)
    .then(function (formattedVisit) {
       console.log(formattedVisit.address);
    }, function (error) {
        // handle error here
    });

This may not look like a big improvement with such a small code block, but let's take a look at how much we can improve the code with sequential operations.

Sequential Operations

Let's say you have multiple asynchronous operations that you would like to chain together. You will notice that the use of promises make for much cleaner, more understandable code.

Using callbacks, we would chain operations like this:

formatVisit(visit, function (formattedVisit) {
    requestVisit(formattedVisit, function() {
        notifyUser(visit.user, function () {
            saveVisit(function(err) {
                if (err) {
                    // handle error here
                    return;
                }
                // handle success here;
            });
        });
    });
});

While with promises, it would look like this:

formatVisit(visit)
    .then(function (formattedVisit) {
        return requestVisit(formattedVisit)
    })
    .then(function () {
        return notifyUser(visit.user);
    })
    .then(function () {
        return saveVisit();
    })
    .then(handleSuccessMethod, function (error) {
        console.log(error);
    });

Imagine a new developer joins your team and has to debug your code. Which sample do you think will be easier to understand and debug? I think we can all agree that the second sample is the best option here.

Error Handling

Handling errors with promises is quite simple and is similar to handling errors in a try / catch block. When chaining promises, you can omit the error callback for all the then methods, except for the last one. When an error occurs in a promise that does not have an error callback, it will skip over all the following success callbacks until it hits an error callback.

In the example above, if an error were to occur in requestVisit(), the following success callbacks would be skipped and the next piece of code to be executed would be the error callback, where we output the error as follows:
console.log(error);

This is similar to a try / catch block where, if an error occurred in the try block, the code execution would jump to the first catch block it encounters.

Parallel Operations

Similar to sequential operations, promises make parallel operations much, much cleaner than with callbacks. For instance, using the Q module, you can use all to turn an array of promises into one promise.

Consider the following sample:

function validateListings(listings) {
  var deferred = Q.defer();
  var promises = [];

  if (!listings || listings.length <= 0) {
      deferred.reject(new Error('Empty listings array.'));
  }

  _.each(listings, function (listing) {
      promises.push(isListingAvailable(listing));
  })

  Q.all(promises)
      .then(function () {
          deferred.resolve();
      }, function (error) {
          deferred.reject(error);
      })

  return deferred.promise;
}

That code will execute isListingAvailable() multiple times in parallel, and the method validateListing will either resolve or reject its promise when all the isListingAvailable() calls have completed. That being said, the error callback will get called at the first sign of failure. That means that whichever of the promises fails first gets handled by the error callback.

Should You Ever Nest Promises?

I personally think that chaining promises outside of the success callbacks (as described in the Sequential Operations section above) is the cleanest approach. However, it is important to note that it is possible to nest promises and that in some situations, it is the preferable approach.

What I mean by nesting promises is that, from within a success callback, we execute an asynchronous operation and handle the promise result from within that same success callback. Let me demonstrate that by an example:

formatVisit(visit)
    .then(function (formattedVisit) {
        return requestVisit(formattedVisit)
    })
    .then(function (success) {
        if(!success) {
            return markVisitAsFailed(visit)
                .then(function () {
                    return notifyAdmin();
                });
        }    
        else
            return notifyUser(visit.user);
    })
    .then(function () {
        // handle success here
    }, function (error) {
        console.log(error);
    });

In this example, if requestVisit() was not successful, we want to call markVisitAsFailed() and then notify the admin, before continuing on with the regular flow. Using nested promises for such a use case is a good solution in my opinion.

Conclusion

To summarize, the goal of this article was to demonstrate the following key points:

  • How promises can avoid the dreaded callback hell.
  • How asynchronous operations can be chained sequentially.
  • How asynchronous operations can be executed in parallel.
  • How errors are handled.
  • When you'd want to nest promises.

I hope this answered some of your questions or concerns regarding Javascript promises, and that it demonstrated how much better everyone's life is when people use promises instead of callbacks. Think about future-you that will have to debug your code in a few weeks or months, and please write cleaner code.

Wissam Abirached

Wissam Abirached