Behind Javascript "Promise"

This article documents my understanding of Javascript "Promise" and how and why it is important, and how it can be implemented using plain Javascript.

1. Background

In Javascript, the word "Promise" does not have the same meaning as the regular English word promise; so Javascript "Promise" does not mean Javascript "promises" (or "weakly guarantees") something.

For some time now, "promise" in Javascript has a very specific, precise, meaning: for example, see here for the commonly accepted definition.

If you come back here confused, you are not alone. I was confused myself, and I didn't start programming yesterday. Certainly not Javascript - I had written "ajax" before the term was invented (it was called DHTML those days). But "promises" baffled me.

If you are in the same situation, I hope this article can bring you closer to your own enlightenment.

2. So what is exactly a "promise"? And in what way it is helpful, useful?

I would begin that "promise" is the wrong choice of word. Well, there as an earlier term for it, called "futures" - which I think is also the wrong choice of word. Both are very confusing, and while I admit that there is a little semantic meaning of English word promise and future still left-over in the Javascript "promise" concept (or construct), it is so vanishingly small that you'd better choose another word that represents the operational meaning. (And no, the word "thenable" is also not helpful).

But let's forget about the name - for all I care you can call it "golly" or "owithurts" or "whydidntithinkofthat".

First, the main purpose of the "golly" - I mean, "promise":

  1. To be able to program/write a chain of asynchronous operations like you write synchronous operations.

That's it.

3. So how is that helpful?

Consider how you write an async operation.

function func1() {
	setTimeout(callback1, 1000);
	function callback1() { do_something1(); }
}
func1();
async func1

or perhaps

function func1() {
	setTimeout(function() {
		do_something1();
	}, 1000);
}
func1();
async func1 with anonymous callback function

(setTimeout may as well be other async operations, such as ajax calls, loading a file, background conversion jobs with webworkers, etc.)

Cool? Now let's chain them - you need to call func2 after func1 is completed, but note that func2 is also an async operation.

function func1() {
	setTimeout(callback1, 1000);
	function callback1() { do_something1(); func2(); }
}
function func2() {
	setTimeout(callback2, 1000);
	function callback2() { do_something2(); }
}
func1(); // func1 will call func2 eventually
async func1 and func2

or

function func1() {
	setTimeout(function() {
		do_something1();
		setTimeout(function() {
			do_something2();
		}, 1000);
	}, 1000);
}
func1(); // func1 will call func2 eventually
async func1 and func2 with anonymous callback function

Compare the above to how you would call func1 and func2 if they were synchronous functions:

func1(); func2();
func1 and func2

Stark difference, isn't it? The main issue with the async function calls above is that you are forced to mix (or embed) func2 in func1. Even two levels is already a mess, if you have a few more levels then it would be very difficult to follow the control flow.

Let's see show "golly" - I mean "promise" helps (without looking into the gory details of how they can be implemented).

func1().then(func2());
async func1 and func2, re-written in promise-style code

I think you will agree the golly-style ... I mean promise-style code is much more easier to follow than chain of callbacks or a nested anonymous functions.

And the most important thing is - while the looks can be deceiving, it is indeed possible to write that way, without doing busy-spin waiting for the first async func1 to finish before invoking func2.

4. And now we interrupt this program for a very important announcement ...

In the interest of responsible disclaimer, before I continue, I need to emphasise that the material that follows, may not be strictly a "promise" in adherence to the "promise" standard.

My understanding did not come from reading other people's "promise" libraries or implementation (in fact I have read none of them). I have only read the examples of how "promises" are used and from very very terse specs (which frankly, does not help much - I very much prefer reading IETF RFCs instead!).

That being said, while the concept will (hopefully) carry to the actual "promise" as used by other "promise" libraries, the detail and the implementation I will present later, will surely not.

The program continues ...

5. Okay I buy the ease of writing/reading, but what goes in there?

Firstly, you need to understand the construct as written above is executed in two steps:

  1. The setup step (done synchronously).
  2. The actual execution step (done asynchronously).

The setup step is done when when the code is first encountered.

alert("a");
func1().then(func2());
alert("b");
async func1 and func2, re-written in promise-style code

This code, will run alert("a"); then run the setup step for func1().then(func2()); then run alert("b");

The setup step does this:

  1. call func1, which must return an object that has a "then()" method (the spec calls this returned object as a "thenable" ... ouch!)
  2. this object's then method will be called immediately passing func2 as the parameter
  3. this then method will keep a record of func2 for further usage
  4. then finally, the async part of func1 will be triggered.

So the setup step, (with exception of the last step), will be executed just like what would be executed if func1() and object.then() are synchronous functions. Indeed, if func1 is not async, the order of execution would be:

  • alert("a")
  • func1()
  • func1()'s result .then();
  • func2()
  • alert("b")
just like what you would intuitively expect.

But we are not interested in the synchronous case. Let's see what is supposed to happen when func1 is async.

First, the setup step is done. Then, after sometime later, the async operation in func1 is completed. When this happens, the whole contraption knows that func2 is to be called.

So you can happily write something like this:

func1().then(func2).then(func3).

The setup step will record both func2 and func3 for later usage, and then func1's async operation is done, it knows to call func2, and when func2 is done, it will call func3. If func2 is async, then func3 will be called immediately after func2, otherwise func2 async operation is triggered, and only when it is done, func3 is called.

In other words, func1, func2, and func3 will be executed, serially, in that order, when each of the function's operation is completed; regardless of whether they are synchronous functions or asynchronous functions. Sweet.

6. You keep talking about the "construct". What happens to the "golly" - I mean, "promise"?

Well, the object returned by func1() is what is called as a "promise", which probably means that it "promises" to do something in the "future" when func1 has completed.

(A "promise" is also known as a "future").

A "promise" obviously has a then() method, and the object returned by then() is also a "promise" --- making it possible to chain all the then's together.

That's it!

7. That's it?

Yes. A "promise" is some sort of contraption that enables you to write a series of callbacks and async functions into a series of then methods which will then be called serially, saving sanity of the programmer.

That's it!

8. Really, that's it? How do you write func1 to return a "promise" then?

Or how do you write func2 and func3, is there any special requirements etc?

The answer of that questions touches on the implementation details of how the "promise" is implemented. By now, you should be well equipped to read the other standard and other "promise" examples to see how they are supposed to be used/programmed (see reference section below).

Instead of re-hashing other people's examples and libraries, the next few sections will delve into a small "promise" library that I wrote myself, based on all the above idea. Nothing beats writing your own implementation if you want to understand the logic behind anything.

So if you read on, your idea of "promise" implementation may not match the standard's - it will match mine instead.

9. Show me.

You have been warned.

Basically, my "promise" object (what I called as "pr") is an object that keeps a list of functions in an array, together with their arguments, and call them in order.

This "list of functions" is entered by calling the then() method of the pr object.

When all the functions have been listed, you can start the "promise" by calling its start() method

This is not in the official spec, the official spec says the promise will start the automatically after the setup step is completed.

The start() method will then invoke each functions in order. Each function indicates its completion by calling a special done() (or fail() method) from its own "this" object.

This is not in the official spec, in fact the official spec says that each of function must be called as a real function with this set to null or to the Global object.

I am violating all these because implementing a "promise" in adherence to the standard is not the objective (there are plenty of those libraries already). My aim was to write a didactic tool, stripped to the bare minimum, with as simple javascript as it can be but, but no simpler, that still implements the concept of "promise". I would like to think that I have succeeded at that goal.

So how does the usage of my version of pr look like?

var o = new pr(func1).then(func2).catch(except2).then(func3).then(func4);
o.start(param);

Notes:

  1. param is the parameter that will be passed to func1.

  2. start will start the entire callback chain.

  3. All the other functions will get the return value of the previous functions as its first parameter.

  4. An async function will look like this. You need to ensure that you call the done() method (giving it the return value to be passed to the next function). Your main function needs to return the this object (which is the pr object) so it can continue with the then chaining.
    function func1(v) {
    	var o = this;
    	// naked callback
    	setTimeout(function(vv) { o.done(vv); } , 500, v);
    	return this;
    }
    

  5. Alternatively, you can wrap your function in a new pr object. If you do this, your main function must return the newly created pr object. The function body of the new pr must return a this object as usual, and it's callback must call this object of the original caller (not the newly created pr).
    function func1(v) {
    	var o = this;
    	// callback wrapped in pr()
    	var ret = new pr(function() {
    		setTimeout(function(vv) { o.done(vv); } , 500, v);
    		return this;
    	});
    	return ret;
    }
    

  6. A synchronous function will look like just a normal function.
    function func1(v) {
    	return v;
    }
    

  7. catch is passed a function that will be called when something failed (if you call this.fail() instead of this.done()).

  8. A function can indicate failure by calling this.fail() instead of this.done(), in which case the function specified in the catch() clause of the will be called, if specified.

    If none is specified, the call chain simply stops there. Note that a catch() clause has impact for all the functions following it: in the example given, if func1 fails, then the pr will simple stops; if func2 or func3 or func4 fail, then except2 will be called. You can cancel an exception handler by calling catch(null).

And here is that pr contraption, in its entirety.

function pr(fn) {
	// we can define these as "vars" but then they are not visible
	// we want them visible so we can inspect them
	this.id = Math.random();
	this.funcs = [];
	this.funcs_args = [];
	this.exceptions = [];
	this.exceptions_args = [];
	this.index = -1;
	this.break = false;

	// prepare
	this.then = function(fn) {
		this.index++;
		this.funcs.length++;
		this.funcs_args.length++;
		this.exceptions.length++;
		this.exceptions_args.length++;
		this.funcs[this.index] = fn;
		this.funcs_args[this.index] = Array.prototype.slice.call(arguments, 1);

		// carry exceptions from previous
		if (this.index > 0) {
			this.exceptions[this.index] = this.exceptions[this.index-1];
			this.exceptions_args[this.index] = this.exceptions_args[this.index-1];
		}
		return this;
	}

	this.catch = function(fn) {
		this.exceptions[this.index] = fn;
		this.exceptions_args[this.index] = Array.prototype.slice.call(arguments, 1);
		return this;
	}

	// execution
	this.start = function(v) {
		this.run_index = -1;
		this.do_next(v);
	}

	this.do_next = function(v) {
		this.run_index++;
		if (this.run_index < this.funcs.length && !this.break) {
			if (typeof(v) != "undefined")
				this.funcs_args[this.run_index].unshift(v);
			this.result = this.funcs[this.run_index].apply(this, this.funcs_args[this.run_index]);

			// 3 possible return types: "this", new instance or pr, and everything else
			if (this.result == this) {
				// do nothing, func's callback will call done/fail
			} else if (this.result instanceof pr) {
				// new pr, join it to ours

				// first, carry exception from current
				for (var k = 0; (k < this.result.exceptions.length) && !(this.result.exceptions[k]); k++) {
					this.result.exceptions[k] = this.exceptions[this.run_index];
					this.result.exceptions_args[k] = this.exceptions_args[this.run_index];
				}

				// then, merge exceptions to our
				Array.prototype.splice.apply(this.funcs,           [this.run_index+1,0].concat(this.result.funcs));
				Array.prototype.splice.apply(this.funcs_args,      [this.run_index+1,0].concat(this.result.funcs_args));
				Array.prototype.splice.apply(this.exceptions,      [this.run_index+1,0].concat(this.result.exceptions));
				Array.prototype.splice.apply(this.exceptions_args, [this.run_index+1,0].concat(this.result.exceptions_args));
				this.do_next(this.result); // pass return values to next function
			} else {
				// anything else (null, undefined, objects, etc)
				this.do_next(this.result); // pass return values to next function
			}
		}
	}

	// callbacks - to be called by delegates
	// if you call this.done yourself, you must return "this"
	// so it does not get called again by do_next
	this.done = this.do_next;

	this.fail = function(v) {
		if (this.run_index < this.exceptions.length &&
		    this.exceptions[this.run_index]) {
			if (typeof(v) != "undefined")
				this.exceptions_args[this.run_index].unshift(v);
			this.result = this.exceptions[this.run_index].apply(this, this.exceptions_args[this.run_index]);
		}
		this.break = true; // stop the chain
	}

	// starting funcs
	if (typeof(fn) != "undefined") this.then(fn);
}
Promise-like implementation in 90-lines

That's it. And this time, that's it, for real.

10. Closing question

Okay, all these have been written for Javascript. But Javascript is hardly the only language with callbacks and async operations. Can I do this on python, perl, Java, C, etc ... ?

And the answer is (obviously) yes. "Promise" or whatever it is called is just a syntactic sugar. It does exactly what a chain of callback does, but all of these are hidden. And the model I outlined above for Javascript can also be implemented in other languages. Javascript has the benefit of closure that makes parameter passing easier - but this can be simulated by other means.

11. Notice

"Promise" has been accepted as a standard feature of Javascript, although still marked as "experimental" (e.g. Mozilla's implementation note here).

This article that I have written here, obviously, represents my understanding of what a "promise" is. I don't claim that it is right one, nor is the proper one. Remember I was baffled at the very beginning, and although I'd like to think that I've got it now, I may still be wrong in the views of the experts and practitioners.

My (simplistic) example implementation of a "promise" does not conform to whatever specification of the "standard promise" (Promise/A+ or whatever the flavour of the day is); the only thing that it conforms to is "it is useful for me".

So take everything I have written here, like everything else in the Internet, with a grain of salt. How much salt you need - I leave that decision to you ☺.

My only wish is that you have gained clearer understanding of what it is for, and when you read other people's (or the standard) implementation of "promise", you know what they are doing.

12. References
There are plenty of Javascript "Promise" articles, they are the trendy stuff right now. Some of them uses jQuery's "Deferreds" which is Promise-like but not exactly a "Promise" according to the "standard"; some libraries that matches more with Promise/A+ "standard".

  1. Mozilla Promise implementation
  2. Promise/A+ spec
  3. Understanding Javascript Promise
  4. Embracing Promises in Javascript
  5. Promises: There and back again