Promises in the Real World

JavaScript promises have become a popular way to handle the tangled mess that JavaScript’s asynchronous nature often creates for us. Promises are very simple but can be very powerful and flexible. In this blog entry, I’m going to show some interesting real-world applications of promises that I’ve used over the last few years that you may not realize are possible when you first learn about promises.

Meme of Doc Brown and Marty McFly at a drive-in from Back to the Future 3

Introduction

A promise is an object that represents the result of an asynchronous operation. A promise can be in one of three states: pending, fulfilled or rejected. All promises start off in the pending state, then at some unknown point in the future, they may transition to either the fulfilled or rejected state. Once the promise changes state, it can never change its state again. When a promise is fulfilled, it usually has a value, and when it’s rejected, it usually has a reason.

You attach callback functions to run when a promise changes state by calling a promise’s “then” method. The first argument is a function to run when the promise is fulfilled, and the second argument is a function to run when the promise is rejected. If a promise is still pending when you attach your callbacks, your callback will be called if and when the state changes. If the promise has already been fulfilled or rejected when you attach your callbacks, the appropriate callback will be run immediately. The fulfilled callback will receive the promise’s value if it has one, and the rejected callback will receive the reason (usually an error) for the rejection.

The classic example of what a promise can do is to turn an asynchronous function that takes a callback into a function that returns a promise object that represents the result of the asynchronous operation. The jQuery library has Ajax methods that take a function argument in the classic callback style, but they also return a promise that can have the callback attached to it via “then”:

$.getJSON('/api/data.json', function (data) {
  console.log(data);
});

$.getJSON('/api/data.json').then(function (data) {
  console.log(data);
});

At first glance, there doesn’t seem to be much difference between the two styles. But because the promise returned by the second example is an object that can be stored or passed around creates a world of difference. When a callback has to be passed into the “getJSON” method, you have to deal with the asynchronous data right there all at once. But when you get a promise back from the “getJSON” method, you can attach multiple callbacks at various points during your application’s life cycle without worrying if the asynchronous operation is finished, all while still being able to access the eventual result.

jQuery “Promises”

The jQuery library introduced promises to its core library way back in version 1.5. This was back in the days when the API and functionality of JavaScript promises were still being debated and standardized, so jQuery’s concept of a promise can be quite different from the modern concept of a promise in JavaScript. For the most part, things work similarly if you don’t dig too deep. However, there are very subtle differences that can lead to extreme differences between jQuery’s promises and that of other libraries. I’ll use jQuery’s Deferred object, which is part of its JavaScript implementation. I’m doing this because jQuery is so ubiquitous and its promise API has been available for years. Most of these examples should translate to other libraries, but be warned there could be differences that cause these examples to break.

You’ll see the latest promise API examples create a promise like this:

var promise = new Promise(function (resolve, reject) {
  setTimeout(resolve, 1000);
});

This example creates a promise that will be resolved without a value after one second. The equivalent example in jQuery looks a little strange and definitely more verbose:

var promise = $.Deferred(function (dfd) {
  setTimeout(dfd.resolve, 1000);
}).promise();

jQuery has an object type called a Deferred that has resolve and reject methods that behave like the resolve and reject functions that are passed to the function you pass to the modern promise constructor. Since jQuery Deferreds can be resolved or rejected, they also have a “promise” method that will return a pure promise object that can’t be manipulated by any other code to resolve or reject its state.

Converting Traditional Asynchronous Libraries to Promises

It’s easy to convert a traditional asynchronously loaded JavaScript library into one that uses promises. One of the libraries I do this with the most is the YouTube API. Their API works by calling a global function you may have named “onYouTubeIframeAPIReady” once the library is loaded. The problem with this is that you have to put all your code that relies on their API inside this one callback function.

var tag = document.createElement('script');

tag.src = "https://www.youtube.com/iframe_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);

function onYouTubeIframeAPIReady() {
  new YT.Player('player1', {});
  new YT.Player('player2', {});
}

On large, complicated pages that have multiple players, this can get unwieldy. But if we convert this into a promise-based approach, we can attach callbacks that will run as soon as the YouTube API is available from anywhere in code that can access the YouTube API promise object:

var tag = document.createElement('script');

tag.src = "https://www.youtube.com/iframe_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);

var YouTubeAPI = $.Deferred(function (dfd) {
  window.onYouTubeIframeAPIReady = function () {
    dfd.resolve(YT);
  };
}).promise();

YouTubeAPI.then(function (YT) {
  new YT.Player('player1', {});
});

//...

YouTubeAPI.then(function (YT) {
  new YT.Player('player2', {});
});

You still have to create a global “onYouTubeIframeAPIReady” function, but instead of doing all your setup inside that function, you can just call the promise’s resolve method with the global “YT” variable. Then you can attach callbacks to the “YouTubeAPI” promise object throughout your code and use it wherever you need it. Your callbacks will be called once the library is loaded.

You can even go one step further with this example and set up a timeout so the promise is rejected after waiting too long for the library to load. This would let you handle that case with your own callbacks.

var YouTubeAPI = $.Deferred(function (dfd) {
  window.onYouTubeIframeAPIReady = function () {
    dfd.resolve(YT);
  };

  setTimeout(function () {
    dfd.reject(new Error('YouTube API failed to load'));
  }, 5000);
}).promise();

YouTubeAPI.then(function (YT) {
  new YT.Player('player1', {});
}, function (e) {
  console.error(e);
});

Now whichever method gets called first – resolve or reject – each will set the promise’s state and call the appropriate callback.

Callback Twice, Promise Once

A common way to animate the scroll position of a webpage with jQuery is to use the following example:

$('html, body').animate({ scrollTop: 2000 }, { complete: function () {
  console.log('complete');
}});

Both the HTML and body elements need to be animated to accommodate browsers that use one or the other to determine the page’s scroll position. This works well, but it will fire the “complete” callback twice – once for the HTML element and once for the body element. I’ve had many times where I needed a callback to fire only once when the scrolling animation was finished. It’s possible to only fire the callback once without promises, but the promise method is very straightforward:

$.Deferred(function (dfd) {
  $('html, body').animate({ scrollTop: 2000 }, { complete: dfd.resolve });
}).promise().then(function () {
  console.log('complete');
});

We’re still calling a callback on the animation’s completion, but now we’re calling the promise’s “resolve” method instead of the actual callback. We’ve moved our callback to the “then” method of the promise. The “resolve” method will fire twice, but the second time it fires the promise has already been resolved and it won’t trigger the attached callbacks again.

This technique is so useful that jQuery even has a built-in way to handle it even more elegantly. Every jQuery object has a “promise” method, which returns a promise that, by default, resolves when all current animations on that object are complete. So the above example can be rewritten even more simply as:

$('html, body').animate({ scrollTop: 2000 }).promise().then(function () {
  console.log('complete');
});

Cancelling Asynchronous Callbacks

Sometimes a user might initiate an asynchronous operation and then initiate the same operation with different parameters before the first one is complete. If both asynchronous operations modify the same element on the page, which one will fire last and modify the page last? If you said the one called last, that’s not guaranteed. Whichever asynchronous operation happens to complete last will fire the callback last and modify the page.

One project I worked on loaded animated GIFs in response to user actions and then inserted them onto a “stage” area on the page when they were done loading. Because the loading was asynchronous, a user could trigger back-to-back operations, but wind up with the first image they chose being loaded into the stage area last. This obviously wasn’t the intended behavior.

Promises to the rescue again!

function loadImage(url) {
  cancelLoadImage();

  return $.Deferred(function (dfd) {
    cancelLoadImage = dfd.reject;

    var img = new Image();

    $(img).on({
      load: function () { dfd.resolve(img) },
      error: dfd.reject
    });

    img.src = url;
  }).promise();
}

function cancelLoadImage() {}

function updateStage(img) {
  $('#stage').html(img);
}

loadImage('/images/animation-1.gif').then(updateStage);
loadImage('/images/animation-2.gif').then(updateStage);

The trick that makes sure the last image loaded will always be the most recent image displayed in the “stage” is to create a promise that will be resolved when the image is loaded. The key step is the “cancelLoadImage” function, which starts off as an empty function that does nothing. Every time “loadImage” is called, it will call “cancelLoadImage.” When a new promise object is created, it will set “cancelLoadImage” to the reject method of the promise, ensuring that each new call to “loadImage” will always reject the previous promise, preventing its resolved callbacks from executing.

Conclusion

Promises are a very powerful way of managing asynchronous behavior in JavaScript. By encapsulating the asynchronous operation in an object that can be passed around, you gain a very powerful and flexible control over that asynchronous operation that you don’t get by simply passing a callback function to an asynchronous method.

The next time you find yourself dealing with asynchronous behavior in JavaScript, remember promises and see if there is a creative solution to your problem using them. I promise you it will change the way you think about the problem.

Resources

The Project Manager-Producer Hybrid