A Simple Promise Implementation in about 20 lines of Javascript

Doug TurnbullFebruary 16, 2014

Promises are a rather convenient way of creating readable asynchronous code. More importantly, implementing Promises is a great way to hone your ability to grok async code. So follow along, you might learn something just like I did!

What’s a Promise?

In short, promises clean up code full of callback chains. Instead of

foo(function success1() {
    bar(function success2() {
        ...
    });
});

You’d rather write more coherent code of the form:

foo()
.then(function success1() {
})
.then(function success2() {
});

The latter reads a bit better, especially as more behavior must be chained together. Specifically, we expect that when foo’s asynchronous bits are done, the success1 function will be called. As we’re chaining promises, we also expect that when success1’s async bits are done, we’ll call success2. Its a lot easier on the eyes than the large triangle of ugly that starts to form is the first example. Flat is better than nested of course!

So how can we implement a Promise? How will foo, success1, and success2 create and return Promises? Moreover, how do we implement “then” so that it both acts as a method of the very first returned promise (ie the Promise instance foo returned) but also somehow connects with the Promise instances that might be created/returned in the yet-to-be called success1 and success2 callbacks? Brain hurt yet?

Working backwards — Implementation of then

The first thing to take apart is our implementation of “then”. Assuming that “foo” has somehow new’d a Promise, and returned it, what happens when we call then on this Promise? Lets sneak a peak:

this.then = function(wrappedFn, wrappedThis) {
  this.next = new Promise(wrappedFn, wrappedThis);
  return this.next;
};

Interesting, then takes another callback (and an optional “this” to bind to the callback when called). With that callback, another promise is created and chained to this one. After this is called:

foo()
.then(function success1() {
})

The Promise returned by foo represents a linked list of size 2

foo's Promise -> promise created by first then

The final “then” is actually a method call on the Promise instance returned by the last then and simply adds another link to the list:

foo's Promise -> promise created by first then -> promise created by second then

A Promise wraps the function passed into it (ie success1 & success2), so our linked-list looks something like:

foo's Promise -> promise created by first then -> promise created by second then
                 (wraps success1)                 (wraps success2)

Remember this is all asynchronous. Setting up this linked-list is done well before any of the wrapped functions are executed. To our linked list, success1 and success2 are entirely black boxes. All we’ve done setup a structure of related bits of work. Next we’ll keep working backwards by seeing how we unravel this work queue.

Fulfilling Promises

So what happens when the async work completes? At some point, foo signals completion on its async work, and that completion needs to cause us to execute success1 — the next link in the chain. Somewhere in foo, where a handle to the promise still exists, foo signals completion, by calling “promise.complete()”.

var foo = function() {
    var promise = ...
    async.onevent(function() {
        promise.complete();
    });
    return promise;
}

The job of promise.complete is going to be to trigger any callback work linked to the next Promise. Its job is to unravel the linked list we’ve built and call success1. This is exactly what we do, by telling our promise to “complete” which causes the next promise to “run”.

this.run = function() {
  wrappedFn.promise = this; // a stupid trick, read below
  wrappedFn.apply(wrappedThis);
};

this.complete = function() {
  if (this.next) {
    this.next.run();
  }
};

Making Promises

A final, but crucial detail. How do we create promises? We can’t simply new a Promise. If we did that, this new Promise would have know to connect it to existing Promise instance in the linked-list we setup before hand. No we need a stupid trick to intercept Promise creation and figure out if its already a link or if its actually a brand new not-yet seen Promise. Here’s that stupid trick:

Promise.create = function(func) {
if (func.hasOwnProperty('promise')) {
  return func.promise;
} else {
  return new Promise();
}

Did you notice earlier in “apply” how we set a “promise” property on the function before calling the wrapped function? This is intended to work in conjunction with “Promise.create”. If the called function calls “Promise.create” we can check the passed in function to see if a promise already exists and reuse that instance. This requires functions calling Promise.create call with themselves as an argument to be able to reuse the correct promise, ie foo looks something like

var foo = function() {
    var promise = Promise.create(foo)
    async.wait(function() {
        promise.complete();
    });
    return promise;
}

This is of course unfortunate, and a bit hacky. Its prone to copy-paste errors as copy-pasters forget to repace “foo” above with their function’s name. I’m sure there’s a more intelligent way to create promises that correctly reuse promises without monkeying with a property on the wrapped function. Please post and let me know if you have any ideas!

And that’s it! Works great. This is a fun little diversion, mostly for my own educational reasons. But I’ve actually been using this as a very simple lightweight implementation in Quepid, our search relevancy workbench. Its worked well. If you’re interested in the full source, check out this gist. And leave comments, let me know if you see something I missed!




More blog articles:


Let's do a project together!

We provide tailored search, discovery and analytics solutions using Solr and Elasticsearch. Learn more about our service offerings