Promised Properties


Three weeks ago I started a new project that uses nodejs and which forced me to learn some asynchronous programming. I couldn’t quite manage to adapt.

Because this was server code that ties together other web services and my own database, many of the helper functions ended up having multiple asychronous steps. The lack of return values strongly pressured me to nest long chains of anonymous functions. As if that wasn’t bad enough, they style also encouraged inlining the details of each step, which made identifying and factoring out common code, all the more difficult. Because of the nesting it was far easier to factor out steps near the end of the chain instead of those in the middle. Compared to conventional synchronous programming, this callback model felt like programming inside out. So I went looking for other options. I watched a video on promises, by the implementors of the Promises/A+ spec and q module.

They pointed out 3 important deficiencies of the async callback model, all of which were contributing to my difficulties:

  1. Syntactical nesting pyramid of doom.
  2. Lack of a callstack when reporting errors.
  3. Lack of return values, who would receive them without a callstack?

The convention synchronous programming model has three ways of returning results (return value, throw error, and side-effects), and the async callback model abandons the two most important ones! Thankfully, promises gave them back to me. I spent a day rewriting all the broken async callback code that I had written, and it’s taken about a week to really grow comfortable with the new style.

Yet, some conveniences are still lacking. The promise chain does not hold temporary values such analogous to having stack-local variables in a multi-step function. We currently achieve the same effect through closures in anonymous functions, as shown in an example at q’s github repo.

function authenticate() {
    return getUsername()
    .then(function (username) {
        return getUser(username);
    })
    // chained because we will not need the user name in the next event
    .then(function (user) {
        return getPassword(user)
        // nested because we need both user and password next
        .then(function (password) {
            if (user.passwordHash !== hash(password)) {
                throw new Error("Can't authenticate");
            }
        });
    });
}

But if promises promote code flattening as an strong benefit, then we ought to support something like stack storage. I propose the addition of two functions:

pput(name)
Take the return value of the current promise as a property with the given name on the current promise and return the current promise.
pget(name1, name2, …)
Retrieve the properties with the given names on the current promise and return them as an array-fulfilled promise, or a single value-fulfilled promise when only one name is given.

All the other promise functions (e.g. then) shall copy the defined properties along the chain. Using promise properties, we can flatten that example code.

function authenticate() {
    return getUsername()
      .then(getUser)
      .pput('user')
      .then(getPassword)
      .then(function (password) {
          // grab the user property from the promise
          if (this.user.passwordHash != hash(password)) {
              throw new Error("Can't authenticate");
          }
       });
}

Notice that inorder to provide both the attribute that was pput on the promise, I also had to change the target reference of the this pointer, so that it points to the promise rather than the global object. I am not sure whether others shall find making that change acceptable or not. If not we can modify the existing chain operations so that they pass the promise in explicitly. So the then function would become pthen. In that case the example would read something like the following.

function authenticate() {
    return getUsername()
      .then(getUser)
      .pput('user')
      .then(getPassword)
      .pthen(function (promise, password) {
          // grab the user property from the promise
          if (promise.user.passwordHash != hash(password)) {
              throw new Error("Can't authenticate");
          }
       });
}

If we want to keep ourselves restricted to only the proposed pput and pget, we’ll just have to do a little extra storage and retrieval work.

function authenticate() {
    return getUsername()
      .then(getUser)
      .pput('user')
      .then(getPassword)
      .pput('password')
      .pget('user', 'password')
      .spread(function (user, password) {
          // grab the user property from the promise
          if (obj.user.passwordHash != hash(obj.password)) {
              throw new Error("Can't authenticate");
          }
       });
}