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.
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":
That's it.
Consider how you write an async operation.
function func1() {
setTimeout(callback1, 1000);
function callback1() { do_something1(); }
}
func1();
or perhaps
function func1() {
setTimeout(function() {
do_something1();
}, 1000);
}
func1();
(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
or
function func1() {
setTimeout(function() {
do_something1();
setTimeout(function() {
do_something2();
}, 1000);
}, 1000);
}
func1(); // func1 will call func2 eventually
Compare the above to how you would call func1 and func2 if they were synchronous functions:
func1(); 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());
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.
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 ...
Firstly, you need to understand the construct as written above is executed in two steps:
The setup step is done when when the code is first encountered.
alert("a");
func1().then(func2());
alert("b");
This code, will run alert("a"); then run the setup step for
func1().then(func2()); then run alert("b");
The setup step does this:
func1, which must return an object that has a "then()"
method (the spec calls this returned object as a "thenable" ... ouch!)
then method will be called immediately passing
func2 as the parameter
then method will keep a record of func2 for further
usage
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:
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.
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.
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!
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!
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.
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
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
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:
param is the parameter that will be passed to func1.
start will start the entire callback chain.
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;
}
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;
}
function func1(v) {
return v;
}
catch is passed a function that will be called when something
failed (if you call this.fail() instead of this.done()).
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);
}
That's it. And this time, that's it, for real.
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.
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.