Tuesday, March 1, 2016

A possible way out of promise hell

I recently have been getting into promises (ie: nodejs, javascript type promises - see the Promise/A+ spec.)
I was reading several articles on this beast and found an exceptionally helpful one in the post entitled "We have a problem with promises" here.
Something told me that this could be explained better.
I believe I have found one possible better explanation.

The confusing thing about promises isn't the syntax but the very important side effects of each function involved.  What a function returns or does effects a promise dramatically.  This is because promises work entirely by side effects.
If we make the types clear, the code becomes much more understandable I think.

So lets set up some hungarian-like conventions for naming our functions and parameters and return values:

Type Prefixes:
p_     a promise
pfn_   a function that returns a promise (a promise generator)
fn_    a function that does not return a value
cbfn_  a commonJS callback that takes (fn_err, fn_success)
apcbfn_ an asynchronous promise callback function that takes (fn_resolve, fn_reject).
v_     a value (not a promise or a function)
vfn_   a function that returns a non-promise, non-function value
tfn_   a function that may throw an error
afn_   an asynchronous function that does not take a cbfn_ (does not return and throws are lost).
ajsfn_ an asynchronous commonJS function that takes a cbfn_ function (does not return and throws are lost).
resolve - a function that is called when a promise is successfully resolved with the value of the promise. Whatever value it returns becomes the value of the promise that called it.
reject -  a function that is called when a promise fails to accomplish its purpose and is called with an error value. Its return value (if any) is ignored by the promise  that called it.
Combine these to say that the value or function may be any of the types specified (if you want to get complicated).
In the "We have a problem with promises" post the author gives a little quiz of code for the reader to understand:
doSomething().then(function () {
  return doSomethingElse();

doSomething().then(function () {


So lets look at the first one with type prefixes added for the cases we want to study.
pfn_doSomething().then(function pfn_resolve() {
  return pfn_doSomethingElse();
What's happening here has several possibilities depending on what each promise ends up doing.
First lets recall how we create a promise:
var p_new = new Promise(apcbfn(pfn_vfn_resolve, fn_reject));
This is really the mother of all promise generators and once this line is run apcbfn() is instantly called.
Realize that apcbfn() is an asynchronous function and thus will never return and any throws it may do will be lost because it runs in a different call stack - except that within a promise, those throws will be caught by a closure and result in a call to fn_reject.  The only way this promise can be rejected or resolved is for one of it's callback functions to be called by the asynchronous function or for it to throw and exception.  Note that if the apcbfn() never calls either of its callback functions and never throws an exception, the promise is powerless to ever be resolved or rejected.
Once the promise is resolved (ie vfn_resolve() is called - if that is the case) its .then() function is called.  If it is rejected, the .then() function is never called - but if the promise has a .catch() function - that will be called on rejection.
Another thing to remember is that promises bubble up.  That means that each .then() in a chain is actually a nesting of .then() functions with the result of the inner .then() being passed up the chain.
This is because a resolved promise holds a value which can be returned from its resolve function. This is how promises can be nested - but the resolve function MUST return a value for this to work.
pfn_doSomething().then(function fn_resolve() {
  return pfn_doSomethingElse();
So above we have a function that returns a promise (pfn_doSomething).  That function, once it creates the promise instantly starts running the promise's apcbfn() function which never returns but because it is inside a promise will have any exceptions caught and passed on to the promise's reject function and thus its .catch() function as well.
The .then() function of pfn_doSomething() will be called only if that promise resolves successfully.
pfn_doSomethingElse() is also a promise generator which is only called upon successful resolution of pfn_doSomething() and once it is called it's corresponding internal apcbfn() function is called - which never returns either but will eventually call its reject or resolve function.  IF the resolve function returns a value, then the pfn_doSomethingElse() get's that value and returns it via fn_resolve() to pfn_doSomething() which will inherit that value internally but not call its fn_resolve() function because it already did before calling the .then() function and promises only call their fn_resolve() functions once by contract.
The reason the quiz given in the "We have a problem with promises" post can be difficult is because we don't know the actual fn_resolve(), fn_reject() and internals of apcbfn() for all the promises mentioned.  In fact, in the syntax of the original quiz, we don't even know what kind of functions the doSomething... functions are.  Here I am assuming they are promise generators and note that explicitly with the prefixes but I also must assume that the apcbfn() functions don't throw errors and that the fn_resolve() functions return values and don't throw errors either.
I think if a newbe to promises like me uses these prefixes in their first coding attempts, things will be easier to follow.
Perhaps this could become a convention to help us all read promise code better.

No comments:

Post a Comment