Behind Javascript "Promise"
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":
- To be able to program/write a chain of asynchronous operations like you write synchronous operations.
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 (done synchronously).
- 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");
This code, will run alert("a")
; then run the setup step for
func1().then(func2())
; then run alert("b");
The setup step does this:
- call
func1
, which must return an object that has a "then()" method (the spec calls this returned object as a "thenable" ... ouch!) - this object's
then
method will be called immediately passingfunc2
as the parameter - this
then
method will keep a record offunc2
for further usage - 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")
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 tofunc1
.start
will start the entire callback chain.- All the other functions will get the return value of the previous functions
as its first parameter.
- 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 thethis
object (which is thepr
object) so it can continue with thethen
chaining.function func1(v) { var o = this; // naked callback setTimeout(function(vv) { o.done(vv); } , 500, v); return this; }
- Alternatively, you can wrap your function in a new
pr
object. If you do this, your main function must return the newly createdpr
object. The function body of the newpr
must return athis
object as usual, and it's callback must callthis
object of the original caller (not the newly createdpr
).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; }
- A synchronous function will look like just a normal function.
function func1(v) { return v; }
catch
is passed a function that will be called when something failed (if you callthis.fail()
instead ofthis.done()
).- A function can indicate failure by calling
this.fail()
instead ofthis.done()
, in which case the function specified in thecatch()
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, iffunc1
fails, then thepr
will simple stops; iffunc2
orfunc3
orfunc4
fail, thenexcept2
will be called. You can cancel an exception handler by callingcatch(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.