How is a promise/defer library implemented? [closed]

Solution 1:

I find it harder to explain than to show an example, so here is a very simple implementation of what a defer/promise could be.

Disclaimer: This is not a functional implementation and some parts of the Promise/A specification are missing, This is just to explain the basis of the promises.

tl;dr: Go to the Create classes and example section to see full implementation.

Promise:

First we need to create a promise object with an array of callbacks. I'll start working with objects because it's clearer:

var promise = {
  callbacks: []
}

now add callbacks with the method then:

var promise = {
  callbacks: [],
  then: function (callback) {
    callbacks.push(callback);
  }
}

And we need the error callbacks too:

var promise = {
  okCallbacks: [],
  koCallbacks: [],
  then: function (okCallback, koCallback) {
    okCallbacks.push(okCallback);
    if (koCallback) {
      koCallbacks.push(koCallback);
    }
  }
}

Defer:

Now create the defer object that will have a promise:

var defer = {
  promise: promise
};

The defer needs to be resolved:

var defer = {
  promise: promise,
  resolve: function (data) {
    this.promise.okCallbacks.forEach(function(callback) {
      window.setTimeout(function () {
        callback(data)
      }, 0);
    });
  },
};

And needs to reject:

var defer = {
  promise: promise,
  resolve: function (data) {
    this.promise.okCallbacks.forEach(function(callback) {
      window.setTimeout(function () {
        callback(data)
      }, 0);
    });
  },

  reject: function (error) {
    this.promise.koCallbacks.forEach(function(callback) {
      window.setTimeout(function () {
        callback(error)
      }, 0);
    });
  }
};

Note that the callbacks are called in a timeout to allow the code be always asynchronous.

And that's what a basic defer/promise implementation needs.

Create classes and example:

Now lets convert both objects to classes, first the promise:

var Promise = function () {
  this.okCallbacks = [];
  this.koCallbacks = [];
};

Promise.prototype = {
  okCallbacks: null,
  koCallbacks: null,
  then: function (okCallback, koCallback) {
    okCallbacks.push(okCallback);
    if (koCallback) {
      koCallbacks.push(koCallback);
    }
  }
};

And now the defer:

var Defer = function () {
  this.promise = new Promise();
};

Defer.prototype = {
  promise: null,
  resolve: function (data) {
    this.promise.okCallbacks.forEach(function(callback) {
      window.setTimeout(function () {
        callback(data)
      }, 0);
    });
  },

  reject: function (error) {
    this.promise.koCallbacks.forEach(function(callback) {
      window.setTimeout(function () {
        callback(error)
      }, 0);
    });
  }
};

And here is an example of use:

function test() {
  var defer = new Defer();
  // an example of an async call
  serverCall(function (request) {
    if (request.status === 200) {
      defer.resolve(request.responseText);
    } else {
      defer.reject(new Error("Status code was " + request.status));
    }
  });
  return defer.promise;
}

test().then(function (text) {
  alert(text);
}, function (error) {
  alert(error.message);
});

As you can see the basic parts are simple and small. It will grow when you add other options, for example multiple promise resolution:

Defer.all(promiseA, promiseB, promiseC).then()

or promise chaining:

getUserById(id).then(getFilesByUser).then(deleteFile).then(promptResult);

To read more about the specifications: CommonJS Promise Specification. Note that main libraries (Q, when.js, rsvp.js, node-promise, ...) follow Promises/A specification.

Hope I was clear enough.

Edit:

As asked in the comments, I've added two things in this version:

  • The possibility to call then of a promise, no matter what status it has.
  • The possibility to chain promises.

To be able to call the promise when resolved you need to add the status to the promise, and when the then is called check that status. If the status is resolved or rejected just execute the callback with its data or error.

To be able to chain promises you need to generate a new defer for each call to then and, when the promise is resolved/rejected, resolve/reject the new promise with the result of the callback. So when the promise is done, if the callback returns a new promise it is bound to the promise returned with the then(). If not, the promise is resolved with the result of the callback.

Here is the promise:

var Promise = function () {
  this.okCallbacks = [];
  this.koCallbacks = [];
};

Promise.prototype = {
  okCallbacks: null,
  koCallbacks: null,
  status: 'pending',
  error: null,

  then: function (okCallback, koCallback) {
    var defer = new Defer();

    // Add callbacks to the arrays with the defer binded to these callbacks
    this.okCallbacks.push({
      func: okCallback,
      defer: defer
    });

    if (koCallback) {
      this.koCallbacks.push({
        func: koCallback,
        defer: defer
      });
    }

    // Check if the promise is not pending. If not call the callback
    if (this.status === 'resolved') {
      this.executeCallback({
        func: okCallback,
        defer: defer
      }, this.data)
    } else if(this.status === 'rejected') {
      this.executeCallback({
        func: koCallback,
        defer: defer
      }, this.error)
    }

    return defer.promise;
  },

  executeCallback: function (callbackData, result) {
    window.setTimeout(function () {
      var res = callbackData.func(result);
      if (res instanceof Promise) {
        callbackData.defer.bind(res);
      } else {
        callbackData.defer.resolve(res);
      }
    }, 0);
  }
};

And the defer:

var Defer = function () {
  this.promise = new Promise();
};

Defer.prototype = {
  promise: null,
  resolve: function (data) {
    var promise = this.promise;
    promise.data = data;
    promise.status = 'resolved';
    promise.okCallbacks.forEach(function(callbackData) {
      promise.executeCallback(callbackData, data);
    });
  },

  reject: function (error) {
    var promise = this.promise;
    promise.error = error;
    promise.status = 'rejected';
    promise.koCallbacks.forEach(function(callbackData) {
      promise.executeCallback(callbackData, error);
    });
  },

  // Make this promise behave like another promise:
  // When the other promise is resolved/rejected this is also resolved/rejected
  // with the same data
  bind: function (promise) {
    var that = this;
    promise.then(function (res) {
      that.resolve(res);
    }, function (err) {
      that.reject(err);
    })
  }
};

As you can see, it has grown quite a bit.

Solution 2:

Q is a very complex promise library in terms of implementation because it aims to support pipelining and RPC type scenarios. I have my own very bare bones implementation of the Promises/A+ specification here.

In principle it's quite simple. Before the promise is settled/resolved, you keep a record of any callbacks or errbacks by pushing them into an array. When the promise is settled you call the appropriate callbacks or errbacks and record what result the promise was settled with (and whether it was fulfilled or rejected). After it's settled, you just call the callbacks or errbacks with the stored result.

That gives you aproximately the semantics of done. To build then you just have to return a new promise that is resolved with the result of calling the callbacks/errbacks.

If you're interested in a full explenation of the reasonning behind the development of a full on promise implementation with support for RPC and pipelining like Q, you can read kriskowal's reasonning here. It's a really nice graduated approach that I can't recommend highly enough if you are thinking of implementing promises. It's probably worth a read even if you're just going to be using a promise library.

Solution 3:

As Forbes mentions in his answer, I chronicled many of the design decisions involved in making a library like Q, here https://github.com/kriskowal/q/tree/v1/design. Suffice it to say, there are levels of a promise library, and lots of libraries that stop at various levels.

At the first level, captured by the Promises/A+ specification, a promise is a proxy for an eventual result and is suitable for managing “local asynchrony”. That is, it is suitable for ensuring that work occurs in the right order, and for ensuring that it is simple and straight-forward to listen for the result of an operation regardless of whether it already settled, or will occur in the future. It also makes it just as simple for one or many parties to subscribe to an eventual result.

Q, as I have implemented it, provides promises that are proxies for eventual, remote, or eventual+remote results. To that end, it’s design is inverted, with different implementations for promises—deferred promises, fulfilled promises, rejected promises, and promises for remote objects (the last being implemented in Q-Connection). They all share the same interface and work by sending and receiving messages like "then" (which is sufficient for Promises/A+) but also "get" and "invoke". So, Q is about “distributed asynchrony”, and exists on another layer.

However, Q was actually taken down from a higher layer, where promises are used for managing distributed asynchrony among mutually suspicious parties like you, a merchant, a bank, Facebook, the government—not enemies, maybe even friends, but sometimes with conflicts of interest. The Q that I implemented is designed to be API compatible with hardened security promises (which is the reason for separating promise and resolve), with the hope that it would introduce people to promises, train them in using this API, and allow them to take their code with them if they need to use promises in secure mashups in the future.

Of course, there are trade-offs as you move up the layers, usually in speed. So, promises implementations can also be designed to co-exist. This is where the concept of a “thenable” enters. Promise libraries at each layer can be designed to consume promises from any other layer, so multiple implementations can coexist, and users can buy only what they need.

All this said, there is no excuse for being difficult to read. Domenic and I are working on a version of Q that will be more modular and approachable, with some of its distracting dependencies and work-arounds moved into other modules and packages. Thankfully folks like Forbes, Crockford, and others have filled in the educational gap by making simpler libraries.

Solution 4:

First make sure you're understanding how Promises are supposed to work. Have a look at the CommonJs Promises proposals and the Promises/A+ specification for that.

There are two basic concepts that can be implemented each in a few simple lines:

  • A Promise does asynchronously get resolved with the result. Adding callbacks is a transparent action - independent from whether the promise is resolved already or not, they will get called with the result once it is available.

    function Deferred() {
        var callbacks = [], // list of callbacks
            result; // the resolve arguments or undefined until they're available
        this.resolve = function() {
            if (result) return; // if already settled, abort
            result = arguments; // settle the result
            for (var c;c=callbacks.shift();) // execute stored callbacks
                c.apply(null, result);
        });
        // create Promise interface with a function to add callbacks:
        this.promise = new Promise(function add(c) {
            if (result) // when results are available
                c.apply(null, result); // call it immediately
            else
                callbacks.push(c); // put it on the list to be executed later
        });
    }
    // just an interface for inheritance
    function Promise(add) {
        this.addCallback = add;
    }
    
  • Promises have a then method that allows chaining them. I takes a callback and returns a new Promise which will get resolved with the result of that callback after it was invoked with the first promise's result. If the callback returns a Promise, it will get assimilated instead of getting nested.

    Promise.prototype.then = function(fn) {
        var dfd = new Deferred(); // create a new result Deferred
        this.addCallback(function() { // when `this` resolves…
            // execute the callback with the results
            var result = fn.apply(null, arguments);
            // check whether it returned a promise
            if (result instanceof Promise)
                result.addCallback(dfd.resolve); // then hook the resolution on it
            else
                dfd.resolve(result); // resolve the new promise immediately 
            });
        });
        // and return the new Promise
        return dfd.promise;
    };
    

Further concepts would be maintaining a separate error state (with an extra callback for it) and catching exceptions in the handlers, or guaranteeing asynchronity for the callbacks. Once you add those, you've got a fully functional Promise implementation.

Here is the error thing written out. It unfortunately is pretty repetitive; you can do better by using extra closures but then it get's really really hard to understand.

function Deferred() {
    var callbacks = [], // list of callbacks
        errbacks = [], // list of errbacks
        value, // the fulfill arguments or undefined until they're available
        reason; // the error arguments or undefined until they're available
    this.fulfill = function() {
        if (reason || value) return false; // can't change state
        value = arguments; // settle the result
        for (var c;c=callbacks.shift();)
            c.apply(null, value);
        errbacks.length = 0; // clear stored errbacks
    });
    this.reject = function() {
        if (value || reason) return false; // can't change state
        reason = arguments; // settle the errror
        for (var c;c=errbacks.shift();)
            c.apply(null, reason);
        callbacks.length = 0; // clear stored callbacks
    });
    this.promise = new Promise(function add(c) {
        if (reason) return; // nothing to do
        if (value)
            c.apply(null, value);
        else
            callbacks.push(c);
    }, function add(c) {
        if (value) return; // nothing to do
        if (reason)
            c.apply(null, reason);
        else
            errbacks.push(c);
    });
}
function Promise(addC, addE) {
    this.addCallback = addC;
    this.addErrback = addE;
}
Promise.prototype.then = function(fn, err) {
    var dfd = new Deferred();
    this.addCallback(function() { // when `this` is fulfilled…
        try {
            var result = fn.apply(null, arguments);
            if (result instanceof Promise) {
                result.addCallback(dfd.fulfill);
                result.addErrback(dfd.reject);
            } else
                dfd.fulfill(result);
        } catch(e) { // when an exception was thrown
            dfd.reject(e);
        }
    });
    this.addErrback(err ? function() { // when `this` is rejected…
        try {
            var result = err.apply(null, arguments);
            if (result instanceof Promise) {
                result.addCallback(dfd.fulfill);
                result.addErrback(dfd.reject);
            } else
                dfd.fulfill(result);
        } catch(e) { // when an exception was re-thrown
            dfd.reject(e);
        }
    } : dfd.reject); // when no `err` handler is passed then just propagate
    return dfd.promise;
};