Understand jQuery Promises and Deferred Objects

At the core of how SuiteCommerce Advanced works are AJAX calls. Asynchronously, requests to send and receive data are made to and from NetSuite while the frontend waits. If we didn't use async JavaScript, this waiting could be blocking: that is, the user would be stuck, unable to do anything until the call is resolved. This is because JavaScript execution is threaded with the rest of what's going on in the browser, which means if one bit of JavaScript is doing something then everything else has to wait for that before they get a turn.

And so, one solution is callbacks. Callbacks are a convention in JavaScript and describe scenarios where you write a function that doesn't produce an immediate result. What callbacks do is wait for the result of the function and then run some code after and, in the meantime, we can do other things.

Perhaps the biggest problem with callbacks is what is colloquially and rather comically referred to as callback hell. It's typified by having code nested in code nested in code nested in code... and so on. For example:

function neverGonnaGiveYouUp(error, something) {
  if (error) {
    console.log('Gave me up')
  } else {
    thing.do(something);
    letMeDown.check(function(error, somethingElse) {
      if (error) {
        console.log('Let me down')
      } else {
        anotherThing.do(somethingElse);
        around.run(function(error, somethingAlso) {
          if (error) {
            console.log('Ran around and deserted me')
          } else {
            alsoAThing.do(somethingAlso);
          }
        })
      }
    })
  }
}

In other words, it's this horrible pyramid where you lose track of what's going on, making it difficult to maintain.

And so on to our solution. While we embrace the idea of modular code, we also make use of promises. In this article, I want to talk generally about this key part of how we code as well as provide some examples. The hope is that when you work on your code, you'll understand why and when to use promises.

States of jQuery.Deferred()

So, using jQuery.Deferred() creates a deferred object. This deferred object has three useful states that we can work with:

  1. Pending — when we're waiting for the call to be either made, resolved or rejected
  2. Resolved — the call we made (usually an AJAX call) has been completed or is otherwise OK
  3. Rejected — the call we made has failed or is otherwise not OK

When writing code, you set up the promise, what you want to happen in those scenarios (if applicable) and then the code that triggers them.

Let's do a little bit of testing. For the purposes of this tutorial, I'm going to use the item search API on my site. Go to your site and open the developer console. In it, paste the following (and modify as necessary):

var keywordSearch = function (keyword) {
  var searchUrl = '/api/items?country=US&fieldset=search&language=en&limit=24&n=3&q=';
  var searchPromise = jQuery.Deferred();

  console.log(searchPromise.state());
}

Then run keywordSearch();. The string pending should be returned. This is because we haven't run anything yet — we haven't made any call to the search API, all we've done is create a promise and left it alone.

Now we can modify this code to actually make the call. Now do this:

var keywordSearch = function(keyword)
{
  var searchUrl = '/api/items?country=US&fieldset=search&language=en&limit=24&n=3&q=';
  var searchPromise = jQuery.Deferred();

  console.log('First check: ' + searchPromise.state());

  var search = jQuery.ajax({
    url: searchUrl + keyword
  })
  .done(function() {
    searchPromise.resolve();
    console.log('Second check: ' + searchPromise.state());
  });

  console.log('Third check: ' + searchPromise.state());
}

Now do a keyword search using the function. As I run a clothing site, I'm going to do keywordSearch('dress');, which results in the following:

First check: pending
Third check: pending
undefined
Second check: resolved

You'll notice that the 'third check' happens before the second one: this is correct. We added in the .done() callback to the .ajax() method. While we were busy making the request, the browser ran the rest of the code, which in this case was the third check. When the request was done, it was called back to run the code contained within the .done().

So what about rejected states? Well, that's pretty easy to set up — all you need to do is the set the request up to fail and chain on a .fail(). Copy and paste this:

var keywordSearch = function(keyword)
{
  var searchUrl = "/api/items?FAILcountry=US&fieldset=search&language=en&limit=24&n=3&q=";
  var searchPromise = jQuery.Deferred();

  console.log('First check: ' + searchPromise.state());

  var search = jQuery.ajax({
    url: searchUrl + keyword
  })
  .done(function() {
    searchPromise.resolve();
    console.log('Second check: ' + searchPromise.state());
  })
  .fail(function() {
    searchPromise.reject();
    console.log('Second check: ' + searchPromise.state());
  });

  console.log('Third check: ' + searchPromise.state());
}

Run your search again and you should see the GET fail and the second check return a rejected state. This is because we messed with the request URL and, recognizing a failed request, the code turned to the .fail() conditions. Great!

In each of these three states, you can run code. With successful and unsuccessful states, you can make the callbacks using .done() and .fail().

There's one final thing which you can add on, which is cool: .always(). This callback runs whenever the deferred object moves from pending to resolved or rejected. You can test it by chaining on another callback:

var keywordSearch = function(keyword)
{
  var searchUrl = "/api/items?country=US&fieldset=search&language=en&limit=24&n=3&q=";
  var searchPromise = jQuery.Deferred();

  console.log('First check: ' + searchPromise.state());

  var search = jQuery.ajax({
    url: searchUrl + keyword
  })
  .done(function() {
    searchPromise.resolve();
    console.log('Second check: ' + searchPromise.state());
  })
  .fail(function() {
    searchPromise.reject();
    console.log('Second check: ' + searchPromise.state());
  })
  .always(function() {
    console.log('All finished!')
  });

  console.log('Third check: ' + searchPromise.state());
}

Then, again, when you run the search, it'll go through all of the callbacks and, regardless of whether the request succeeded or not, it will print our message.

The point is that there are two important methods, .resolve() and .reject(), and three events — done, fail and always. With this, we can do work and have code wait for certain things to be true.

Wait, Do I Even Need .Deferred()?

You'll note in the above example, that all I'm actually using .Deferred() for is status reporting in the console logs. What's the point in that?

Indeed, if you look through the SCA code, you'll see numerous examples of promises that don't evoke .Deferred() at all. Part of the reason for this is that any time you make an AJAX call using jQuery, it is treated as a deferred object. The callback features we just talked about are made available to it automatically.

Well, it comes down to how you write your code — the more complex it is, the more you may want to enter the foray of deferred objects.

In the previous example, all we did was use the deferred object to use it as a way of returning a status, but we didn't actually use the deferred object itself — for this, we used the AJAX call — but what if the AJAX call is just one of many things going on at once? You can then use the deferred object as the main trigger point. Put the following code in your console:

var keywordSearch = function(keyword)
{
  var searchUrl = '/api/items?country=US&fieldset=search&language=en&limit=24&n=3&q=' + keyword;

  var searchResult = jQuery.ajax({url: searchUrl})

  return searchResult;
};

var returnTotal = function(searchResult)
{
  return searchResult.total;
};

var reporter = function(result)
{
  return console.log(result)
};

var countItems = function(keyword)
{

  var result
  , countPromise = jQuery.Deferred();

  countPromise
  .done(function(searchResult)
  {
    result = returnTotal(searchResult);
    reporter(result);
  })
  .fail(function(error){
    reporter(error);
  });

  keywordSearch(keyword)
  .done(function(searchResult)
  {
    countPromise.resolve(searchResult);
  })
  .fail(function()
  {
    countPromise.reject('Whoops!');
  });
};

So with this code, we've separated it out into four functions — in your SCA code, you're going to have a number of functions per file and quite a few per module, so this makes sense. We want to separate out the distinct functions. The new code for this part of our pseudo-module, and one of the functions is to count the number of results and return it (or log it in the console, in this case).

We set up the countItems function with effectively two promises: one using jQuery.Deferred() and the one from the AJAX call from performing the search. Note that we've moved the latter's promise out of its function and into the final function. This allows us to add in the successful/fail conditions here, as well as what triggers them. In our previous example, we didn't pass anything into the .resolve() and .reject() but this time we have, specifically:

  • The arguments in .resolve() are passed to the .done() of the count promise and is triggered when the keyword search completes successfully
  • The arguments in .reject() are passed to the .fail() of the count promise and is triggered when the keyword search does not complete successfully

This is a simple example, made slightly convoluted with some verbose code, but you should see how everything links together. You should also see how much easier this is then writing a massive list of of nested callbacks, which was the point of the exercise.

We didn't even add in proper error and exception handling, so you can see how this could get more complicated if we did.

When and Then

Promises can be difficult to get your head around, and there are still aspects of them that we haven't covered. One final thing we can look at are two jQuery methods: .when() and .then().

These two methods are alternative ways to execute callbacks. Running jQuery.when() processes the deferred objects passed to it (for example, AJAX requests). When the promise is resolved (or rejected), the appropriate callbacks are called. When you add in more than one deferred object, a new 'master' deferred object is made that tracks the aggregate status of the other promises. When these are resolved, the master promise will be. What you can then do is put one or more .then() methods afterwards to process the promise.

Let's look at one final bit of code:

var keywordSearch = function(keyword)
{
  var searchUrl = '/api/items?country=US&fieldset=search&language=en&limit=24&n=3&q=' + keyword;

  var searchResult = jQuery.get(searchUrl);

  return searchResult
};

var delayedResponse = function(delay)
{
  // Calls a web service offering delayed responses to GET requests
  var delayedPromise = jQuery.Deferred();

  var response = jQuery.get('https://httpbin.org/delay/' + delay)
  .done(function(){
    delayedPromise.resolve();
  });

  return delayedPromise.promise()
};

var countItems = function(searchResult)
{
  // Doesn't actually count results, just reads the returned total
  var count = searchResult.total;

  return count
};

var listItems = function(searchResult)
{
  // An additional function to pass the time
  var items = [];

  for (i in searchResult.items) {
    items.push(searchResult.items[i].displayname);
  }

  return items
};

var masterSearch = function(keyword, delay)
{
  // Our 'big' function that performs all those calls
  var count, items;

  jQuery.when
  (
    keywordSearch(keyword)
  , delayedResponse(delay)
  )

  .then
  (
    function(masterPromise)
    {
      count = countItems(masterPromise[0]);
      items = listItems(masterPromise[0]);
    }
  )

  .then
  (
    function()
    {
      console.log('Your search for ' + keyword + ' returned ' + count + ' items, including ' + items)
    }
  );
};

You can then run the search by passing two values to the final function, eg: masterSearch('dress', '5'), which should result in the following appearing after five seconds:

So a few new things in this code worth talking about. Firstly, because we needed another AJAX call (and preferably one that took a little while), I've added a function that calls an open API on httpbin.org, a website that offers (among other things) the opportunity to call it and have it respond after a number of seconds that you specify.

As you can see in the first part of jQuery.when() statement, we've specified two promises that we want to comprise our master promise. Simultaneously, we perform the keyword search and the call to the 'delayed response' service. Upon resolution, the deferred object is passed to the function in the first .then() method. We run our count and generate a list of items. We then run another .then(), because why not? This just takes those variables and prints them in the developer console.

What's interesting about the .then() method is that it returns a promise each time, this means that if you want to do things sequentially you can separate them out using numerous instances of .then().

You'll also note that we didn't use .done() or .resolve() in the masterSearch function — we didn't need to in this particular scenario, but you'll also note that we don't have any error or exception handling, so .reject() and .fail() could be something we would need to add in proper code.

Final Thoughts

Promises allow to do something once something has finished do something else, all the while not getting in the way. There are a number of ways to implement this, including:

  1. Status reporting on some code, if you don't want to use the AJAX request itself. This could be useful if you need other things to be done before resolution conditions are triggered.
  2. Status reporting on an AJAX request itself, as they are deferred objects themselves.
  3. Building up into bigger promises, so that actions can be performed once a number of promises have been resolved.

The examples I've offered in this article have been simplified versions of what you can expect in SCA code, but they are themselves sometimes deliberately complicated to not completely divorce them from reality.

As deferred objects can be created arbitrarily with jQuery, there is no need to tie them to AJAX calls and I could have offered snippets where, for example, promises are made and resolved when an animation is completed, or when some other DOM event has completed. If you want an incredibly pure example, just use the following:

var p1 = jQuery.Deferred()
, p2 = jQuery.Deferred();

jQuery.when
(
  p1, p2
)
.then
(
  function() {
    console.log('Promises complete!')
  }
)

Resolve p1 and p2 and it'll print the message in the console. You could use this as skeleton code, and build around it.

As for the SCA code itself, there are numerous examples of promises throughout the code. While you would not make search API requests via URLs like I did in these examples, we use promises in retrieving the cart, facets, item details, order information, and others. If you're interested in looking at real examples, I would recommend using grep and going through the suitecommerce directory for things like "promise" and "deferred".

Those who have done some of the tutorials will have remembered that both the artists module and the testimonials module feature promises, and you can revisit them for other examples of promises in an SCA context.

I will include links to some external resources that you may find useful as I myself recognize that this can be a difficult thing to understand.

More Information