Promises in JavaScript – Documentations and Examples

Hi my name is Guillermo Antony Cava Nuñez, recently while working with sweetalerts2, had to handle some Ajax requests and wanted to make use of the preconfirm methods available. It uses promises in order to wait for the response prior to advancing to the next, thus had to make a promise within the scope of the method. This drove me to refresh and pick up on promises, thus the articles that follows.

What’s a promise?

Operationally, promises are composable wrappers for callbacks. They’re useful for sequencing synchronous and asynchronous actions. Promises let you start with code like this:
$.ajax({
  url: '/value',
  success: function(value) {
    var result = parseInt(value) + 10;
    setTimeout(function() {
      console.log(result);
    }, 5000);
  }
});

…and refactor it into this:

ajax({ url: '/value' }).then(function(value) {
  return parseInt(value) + 10;
}).then(
  delay(5000)
).then(function(result) {
  console.log(result);
})
Maybe it’s not clear why the bottom version is better—it’s because there’s no nesting. Imagine if the example were much larger. The top version might have a dozen nested callbacks, which would be hard to read and hard to rearrange. The bottom version would be a straightforward sequence of actions, not very different from how the code would look if it were synchronous.
A promise supports two methods: then and catchthen takes a continuation, which is just a callback that takes the result as an argument and returns a new promise or any other value. Similarly, catch is the callback that is invoked when the action raises an exception or fails in some other way.
The full version of then encompasses both behaviors:
promise.then(onFulfilled, onRejected)

So catch can be thought of as being defined as follows:

promise.catch(onRejected) := promise.then(null, onRejected)
Note: It doesn’t matter if the callbacks are attached before or after the promise has been fulfilled. If the callback is attached after the promise is fulfilled, it will still be called.

How do you make a promise?

You don’t have to build an object with thenandcatch methods to make a promise. You should use the Promise constructor instead:
var promise = new Promise(function(resolve, reject) {
  // maybe do some async stuff in here
  resolve('result');
});
You just have to call the resolve callback when your promise is fulfilled, or call reject if something goes wrong. Equivalently, you can just raise an exception.
If you want to wrap a value in a promise that resolves immediately, you can simply write Promise.resolve(value). Or if you want to make a promise that fails immediately, you can write Promise.reject(error).

Timeouts

The setTimeout function is used to execute some code after a specified delay. I’ve found it’s really useful to define a promise version of it:
function delay(milliseconds) {
  return function(result) {
    return new Promise(function(resolve, reject) {
      setTimeout(function() {
        resolve(result);
      }, milliseconds);
    });
  };
}

This is how you use it:

delay(1000)('hello').then(function(result) {
  console.log(result);
});

You might wonder why delay is curried. This is so we can sequence it after another promise:

somePromise.then(delay(1000)).then(function() {
  console.log('1 second after somePromise!');
});

AJAX

AJAX is an archetypal use case for promises. If you’re using jQuery, you’ll find that $.ajax isn’t quite compatible with promises, because it doesn’t support catch. But we can easily create a wrapper:
function ajax(options) {
  return new Promise(function(resolve, reject) {
    $.ajax(options).done(resolve).fail(reject);
  });
}

Example:

ajax({ url: '/' }).then(function(result) {
  console.log(result);
});

Or, if you don’t use jQuery, you can make a wrapper for XMLHttpRequest:

function ajax(url, method, data) {
  return new Promise(function(resolve, reject) {
    var request = new XMLHttpRequest();
    request.responseType = 'text';
    request.onreadystatechange = function() {
      if (request.readyState === XMLHttpRequest.DONE) {
        if (request.status === 200) {
          resolve(request.responseText);
        } else {
          reject(Error(request.statusText));
        }
      }
    };
    request.onerror = function() {
      reject(Error("Network Error"));
    };
    request.open(method, url, true);
    request.send(data);
  });
}

Example:

ajax('/', 'GET').then(function(result) {
  console.log(result);
});

Deferred execution

Question: in which order do the strings get printed in this code?
new Promise(function(resolve, reject) {
  console.log('A');
  resolve();
}).then(function() {
  console.log('B');
});
console.log('C');
Answer:A,C, then B.
Is that surprising to you? It was a little surprising to me. The thenthunk is deferred, but the one passed to the Promise constructor isn’t.
But we can take advantage of the behavior of then. I often want to defer the execution of some piece of code until after the current synchronous code is finished. I used to use setTimeout(func, 1) to do this. But now you can make a promise for it:
var defer = Promise.resolve();

You can use it like this:

defer.then(function() {
  console.log('A');
});
console.log('B');
…which prints B, then A. Although this isn’t any shorter than just using setTimeout(func, 1), it clarifies the intention and is composable with other promises. The fact that it can be chained with other promises leads to better structured code.

Closing remarks

The true value of promises is realized when all asynchronous code is structured with them. By making composable promises for things like timers and AJAX actions, it’s easy to avoid nesting and write more linear code.
A few things were surprising to me:
  1. The callbacks for thenandcatch can return any value, but they behave differently if the value is a promise. Ordinarily, the return value is passed to the next then continuation in the chain. But if the return value is a promise, the promise’s fulfillment value is passed to the continuation instead. This means returning Promise.resolve(x) is the same as returning x.
  2. The function passed to the Promise constructor is executed synchronously, but any continuations sequenced with thenorcatch will be deferred until the next event loop cycle. The latter behavior is why defer works.
  3. catch is a reserved keyword used for exception handling in JavaScript, but it’s also the name of the method to attach an error handler to a promise. It seems like an unfortunate naming collision. On the bright side, it’s easy to remember; so maybe the collision isn’t unfortunate after all!
Surprise #1 is weird to me because of the following thought experiment: what if you actually want to pass a promise to the next callback in the chain? If you try to do so naively, the promise will be automatically unwrapped and you won’t get the expected result. One might ask: why would anyone want to pass a promise to the callback? Well, because maybe the code doesn’t know what kind of value it’s passing to the callback. Maybe the value is produced elsewhere in the program, and it’s supposed to just be threaded through. Now you have to first check if it’s a promise, and if so wrap it in some kind of other thing to prevent it from being unfolded in transit. So I would prefer that the callbacks always have to return a promise (even if it’s just a value wrapped in Promise.resolve(value)).
Having thought about surprise #2 for a moment, I’ve come to the conclusion that the callbacks have to be deferred. Here’s why: suppose a promise fails. It needs to call the next error handler in the chain, or throw an exception if there isn’t one. But it has to wait for the error handlers to be attached in the first place. So how long does it wait? The most natural answer is that it waits until the next iteration of the event loop.
Despite these oddities, promises are a welcome addition to JavaScript. Callback hell was one of my least favorite parts of writing JavaScript, but now it’s a non-issue.
P.S. Promise is a monad! Or at least it would be, if it weren’t for surprise #1.

Leave a Reply

Your email address will not be published. Required fields are marked *