The ins and outs of Javascript Promises


When I first saw the promises specs and early shims for JavaScript, I could not see the benefits. With careful use of callbacks in closures, the code was less obscure that a chain of .then functions – particularly in CoffeeScript. Yes, there was unified error handling, but that was not enough to encourage change.

In the last year two additional factors encouraged me to change my mind.

  1. Promises are now native to all modern browsers (and node).
  2. Lispz, as a functional language with an excess of parentheses brings back the callback hell of pure JavaScript (over CoffeeScript).

Last week I had a problem where promise rejection deep with a promise stack were going to window.onerror rather than passing up the line. A friend and I spent a couple of hours on understanding promises better. Here are some of our findings.

Basic Promises look like callbacks

var summer = function(accumulator) {
  return new Promise(function(resolve, reject) {
    setTimeout(function(){resolve(accumulator + 1)},1)
  })
}

var show = function(accumulator) {
  console.log(accumulator)
}

summer(0).then(show) // ==> 1

In the example above, using the setTimeout callback would produce less code with only a little loss of clarity..

Promises chain

Life gets more interesting when you need a sequence of asynchronous expressions to occur in order where each event requires information from a previous event. In other words, making asynchronous code synchronous.

summer(0).then(summer).then(summer).then(show) // ==> 3

This is called “callback hell” because each callback is inside the one before causing an unreadable level of indented code.

var summer = function(accumulator, cb) {
  setTimeout(function(){cb(accumulator + 1)},1)
}

var show = function(accumulator) {
  console.log(accumulator)
}

summer(0, function(accumulator) {
  summer(accumulator, function(accumulator) {
    summer(accumulator, function(accumulator) {
      show(accumulator)
    })
  })
})

Now picture the above with code that actually does something. As clear as mud?

Promises can stack as well as chain

If the code passed to a then returns a promise, this is added to the promise chain as you see above. There is nothing stopping the returned promise to be a chain – and so on to any depth. It may not seem much of a difference, but is critical to flow control. In a more complex async interaction, promises from otherwise unconnected functions need to be called in a controlled way.

Below is a complex example in Lispz for reading and compiling files from GitHub. When in Lispz converts to then in JavaScript. By convention I add > to library functions that return a promise.

    (ref lispz-js    (repo.read> "lispz.js"))
    (ref groups      (when repo.entries> (group @)))
    (ref modules     (when groups  (build-modules @.modules)))
    (ref riots       (when groups  (build-riots @.riots)))

    (when (promise.all modules lispz-js riots) [sources]
      (ref  code  (list.flatten [["window.lispz_modules={}\n" sources]]))
      (return (repo.write> "ext/lispz.js" (code.join "") "lispz release code")
      )
    )

This may be a hard read for the non-lisp-aware. Groups returns a promise that when fulfilled give a list of files in GitHub. When this promise fulfills the build functions are called to read the files and store them. Promise.all is a JavaScript Promises function that resolves when all in the list resolve. In this case one everything is read the target file can be written.

Then and Catch always return a promise

It doesn’t matter what you return from a then clause – it will always be wrapped in a promise. On consideration, this is obvious. Since then is only called once a promise is fulfilled it only has the option of returning a promise.

summer(0).then(summer).then(function(){ return 23 }).then(show) // ==> 23

This is useful for caching. The first call is asynchronous – loading data from the server. All later calls read from the cache and can return the results immediately.

function load(file_name) {
  if (cache[file_name]) return cache[file_name]
  return new Promise(function(resolve, reject) {
    load_from_server(file_name, function(err, result) {
      if (err) return reject(err)
      resolve(result)
    })
  })
}

Exceptions in a promise turn into rejection

The reject function is tightly coupled to the promise. In practice the asynchronous action may be levels need in the call stack. Do we need to pass the reject function down to everyone who may need it. This is not viable if you are using libraries you did not write. Fortunately the designers of Promise-A+ accounted for this. Inside a promise an exception thrown is limited to the promise context and is converted into a call to reject. So, throw away.

There is a caveat. If you have a call-back inside a promise, any exceptions thrown inside the callback is in a different context. They are not converted to promise rejections. This is the only time you will need try and catch.

 

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s