Get More From jQuery Promises

Note: jQuery promises are probably not what you want to use at this point (January, 2016). Look into ES2015 promises or libraries that conform to that spec.

Intro: Promises and deferred objects were introduced to jQuery way back in version 1.5 as part of a re-write of the AJAX module. I’ve only started using them in earnest in the past few months while building LMS, but they’ve become essential to keeping asynchronous code readable and maintainable. There are already a lot of resources available on this subject, but here I’ve included all of the useful details I picked up from different places while learning the ins and outs of using jQuery’s promise implementation.

What Is A Promise Worth?

A promise represents the result of a single execution of a task that will complete at a time unknown to the caller. It could complete immediately, in the future or it could have already been completed. In code a promise object is used to register callbacks to be executed when the state of that task changes and to manage higher-level workflow, but never to change the state of the task.

Promises From jQuery

Since promises were introduced as part of the AJAX re-write the go-to example is the $.ajax family of methods which now return promises in addition to the original API of accepting success and failure methods in the configuration object:

// Original API
$.ajax({
  url: 'http://example.com/fakeapi',
  success: function(data, textStatus, jqXHR) {},
  error: function(jqXHR, textStatus, errorThrown) {},
  complete: function(jqXHR, textStatus) {}
});

// With Promises
$.ajax({ url:'http://example.com/fakeapi' })
  .then(function(data, textStatus, jqXHR) {
    // success
  })
  .fail(function(jqXHR, textStatus, errorThrown) {
    // error
  })
  .always(function() {
    // complete
    // arguments will mirror success or error as called
  });
Comparing the AJAX APIs

It looks a little cleaner, but what more can we do with a promise object?

Chaining Promises

If a promise method like then returns another promise, so you can chain successive calls to form a descriptive timeline of tasks:

// Promise to get weather
$.getJSON('http://weather.com/forecast/80001')
  .then(function() {
    // Get the text description of the forecast
    var description = data.forecast[0].description;

    // Return promise to get photos
    return $.getJSON('http://photos.com/search/' + description);
  })
  .then(function(data) {
    var photos = data.photos;

    // Display the photos
  });
Chaining promises for easy-to-read async code

The promise API also includes a function called pipe which was originally different from then for “pre-filtering” the results of a promise, but as of 1.8 then === pipe, as discussed on Stack Overflow.

In any case, using then is much cleaner than it would have been with configuration objects and nested callbacks, but we can take this further.

Getting Multivariate

What if we have multiple task-based dependencies, but they don’t depend on each other?

// Parameter list of promises
$.when($.get('google'), $.get('bing'))
  // All finished, results in same order
  .then(function(googleData, bingData) {
    // Now compare results
  });
Combining multiple promises into an aggregate

Now we’re getting somewhere! Coordination of multiple asynchronous tasks would otherwise be difficult to write, difficult to read and difficult to debug, but by using promises everything is clear and concise.

Say “When?”

At some point you will want to conditionally include promises in the parameter list to when. One way to do this is to add promises to an array and then use function.apply to match the method signature:

  $.when.apply($, arrayOfPromises);
Applying `when` to an arbitrary number of promises

This will work, but has a few downsides:

  • It’s an awkward expression to remember and repeat
  • It’s no longer immediately clear what we’re waiting on
  • If you’re expecting results from these promises they will now be given to the callback in an unpredictable order

The first problem can be mitigated by writing a function that does this directly like Q’s all method (Q is another JavaScript promises implementation), but there’s an approach that has none of these downsides which I prefer.

When you pass an object that isn’t a promise to when (determined by the existence of a promise function on the object) it’s interpreted as being an immediate result that will be passed through to then just like a successful promise result. By taking advantage of this behavior we can maintain clarity and avoid adding another method to the API:

function getTemplate(premiumAccount) {
  if(!premiumAccount)
    return 'free template';

  // Promise the premium template
  return $.get('premiumTemplate');
}

function uploadResume(resumeFile, premiumAccount) {
  return
    $.when(uploadResume(resumeFile), getTemplate(premiumAccount))
      .then(function(resumeMediaKey, template) {
        // Ready to display
        return renderTemplate(template, resumeMediaKey);
      });
}
Combining promises and plain objects

Making Your Own Promises

Now that we know how to use promises, let’s make some of our own using deferred objects.

A deferred object can do all that a promise can plus change the state of the task. You won’t see jQuery return a deferred object from anywhere but the factory method $.Deferred because the state of a task should only be changed by the code that has implemented that task.

Once we’ve created our deferred object, there are three methods that you’ll likely need to use:

  • resolve - mark this task as having completed successfully, optionally passing values
  • reject - mark this task as having failed, also with optional values
  • promise - get the more targeted promise object for attaching handlers to state changes

Here they are in a quick example:

function upload(fileName) {
  var uploading = $.Deferred();

  // Imaginary file upload API
  file.upload(function(err, mediaKey) {
    if(err) {
      // Change to error/failed state
      uploading.reject(err);
    } else {
      // Pass the generated key to observers
      uploading.resolve(mediaKey);
    }
  });

  return uploading.promise();
}
Using resolve, reject, and promise

If you want even more control over how the success or failure methods are called, resolveWith and rejectWith set the first parameter as the context for executing those functions, meaning that within the callback this will point to the object you provide instead of the deferred object.

There’s one more feature of jQuery’s promises that seems useful, but I haven’t seen used much: progress notifications. Deferred objects have a notify method (and notifyWith as above) that will send data to progress handlers.

// Task-based code with deferred object "fileUploading"
file.updating(function(bytesReceived) {
  uploading.notify(bytesReceived);
});

// Calling code, could also be the third parameter to "then"
uploadFile(fileToUpload)
  .progress(function(bytesReceived) {
    console.log(bytesReceived + ' B / ' + file.totalBytes + ' B');
  });
Notification via promises

Now let’s put everything together:

// Imaginary file upload API
var file = {
  totalBytes: 300,
  upload: function(fileName) {
    var uploading = $.Deferred(),
      bytesTransferred = 0;
      transferringInterval = setInterval(function() {
        bytesTransferred += 50;

        uploading.notify(bytesTransferred);

        // Simulate error
        if(Math.random() > 0.95) {
          // Change to error/failed state
          uploading.reject('Simulated network error');
        }

        if(bytesTransferred >= file.totalBytes) {
          // Pass the generated key to observers
          clearInterval(transferringInterval);

          uploading.resolve('MEDIAKEY');
        }
      }, 250);

    return uploading.promise();
  }
};

$(function() {
  var $console = $('#console'),
    $uploadButton = $('#upload');

  $uploadButton.click(function(){
    $uploadButton.attr('disabled', 'disabled');

    $console.empty();

    file.upload('thefile')
      .then(
        // Success
        function(resourceKey) {
          $console.append('<li>File uploaded successfully with key ' + resourceKey + '</li>');
        },
        // Failure
        function(err) {
          $console.append('<li>File failed to upload: ' + err + '</li>');
        },
        // Progress
        function(bytesReceived) {
          $console.append('<li>' + bytesReceived + ' B / ' + file.totalBytes + ' B</li>');
        })
        .always(function() {
          $console.append('<li>Complete.</li>');
          $uploadButton.removeAttr('disabled');
        });
  });
});
File uploading example

Wrapping Up

That’s promises in a nutshell. Remember that they can be used for more than coordinating network related code: you can model workflows like checkout processes, manage complex DOM interactions like animations, and more. I hope this introduction has given you the tools to appreciate and use promises in your own code.