
JavaScript Promises in Wicked Detail - callum85
http://mattgreer.org/articles/promises-in-wicked-detail/
======
exogen
The situation caused by promises gaining popularity and non-IE browsers
refusing to implement setImmediate is really quite ugly. setTimeout's minimum
value is defined to be 4ms by the HTML5 spec, so if the browser conforms to
that and your app makes heavy use of promises (using setTimeout), now you're
waiting multiple screen refreshes for updates – so you don't actually want to
use setTimeout. Even though the asynchronous requirement is good for
developers using the promise API, it really screwed over implementors.

The setImmediate polyfills attempt a bunch of ugly fallbacks, including
(depending on which one you use): yield/generators, MutationObserver,
postMessage/MessageChannel, onreadystatechange, and finally setTimeout (the
slowest).

IMO the browser vendors should just give in and add setImmediate – if ever
there was a need for it, this is it. But maybe they consider ES6 features a
good enough substitute, I don't know.

~~~
lhorie
This. Ran into this very issue when I was implementing promises in Mithril (
[http://lhorie.github.io/mithril](http://lhorie.github.io/mithril) ). This
really makes promise performance a much uglier beast than it needs to be.

I took a less conventional road there: I deliberately violate the A+ spec and
let surprises like the ones in the article example happen, in order to get
good performance without hacks/bloat. Typically, promise resolution is usually
done asynchronously anyways (otherwise, why bother using promises?), so this
caveat would only be a problem in some serious case of misuse.

In any case, surprises are fixable in application space (i.e. just move lines
of code around), but performance degradation due to some deep implementation
detail in the promise API / browsers not playing ball is nearly impossible to
fix unless you switch to a non-conformant promise library (in which case,
you'd have to fix the surprises anyways - assuming you had any cases of misuse
to begin with).

~~~
spion
A+ doesn't require that you use the browser's asynchronous scheduling
mechanism, it only requires that the promise callback is always executed
_after_ the current function. There is a really neat hack to achieve this
without completely relying on the browser's scheduler by implementing your own
callback queue.

    
    
      asap(callback);
    

which will push to an array of callbacks that you need to execute and then
call a loop if one isn't currently active:

    
    
      function asap(cb) {
        callbacks.push(cb);
        setTimeout(flushQueue, 5);
      }
      

Flushqueue will run all the callbacks from the queue in a loop, then empty it

    
    
      function flushqueue() {  
        if (flushing) return;
        flushing = true;
        for (var k = 0; k < callbacks.length; ++k) {
          callbacks[k]();
        }
        flushing = false;
      }
    

Now if some of the callbacks schedules a new function to execute using asap

promise.then(function first(x) { return alreadyResolvedPromise.then(function
second(y) { return y.some.property; }); });

then when "promise" resolves, first()'s execution will push a new function to
the queue, changing queue.length, causing that loop above to cause one more
iteration - but only _after_ first is complete.

(If you don't nest the callbacks, then the push will happen before flushqueue
even starts executing)

The code above is overly simplified but I think its enough to demonstrate the
idea. (Huge thanks Stefan Penner for explaining it to me :D)

~~~
exogen
Unfortunately there's no way to remove the need for flushQueue being called
with the browser's asynchronous scheduling mechanism (using setTimeout in your
example) in the case where it isn't already flushing – that's the issue.
Flushing the callback queue to completion is an optimization, not really a
fix.

~~~
spion
Yes, truly asynchronous things which took less than 5ms will now take at least
5ms to execute. But its a stretch to say that for Promises/A+ implementations

"... always require at least one more iteration of the event loop to resolve.
This is not necessarily true of the standard callback approach."

as the article states. A fully synchronous chain of 20 operations wont take
100ms - it will only take 5ms

~~~
lhorie
>> it will only take 5ms

This is true in the scenario of a linear chain, e.g. `.then(foo).then(bar)`,
but the spec also requires that the following should work (for interop
purposes, among a few other scenarios):

    
    
        .then(function() {return someOtherpromise.then(doSomething)})
    

So you might incur other harder-to-track-down penalties if you're doing
interop between libraries somewhere, or if the promise chain has dependencies
on the data provided by upstream resolvers.

~~~
spion
If someOtherpromise is an already resolved promise from your own library, then
it will still take just 5ms - the neat hack here is that asap will simply push
another callback at the end of the queue (which is still being processed by
the for loop) and the loop will simply get one more iteration straight in the
middle of its execution.

If its from another library, then that other library may be using its own
scheduler and you will loose those 5ms either way...

~~~
lhorie
Yep. I believe the bigger promise libraries (bluebird et al) implement
mechanisms similar to asap internally iirc.

My point was that violating the A+ spec instead of getting into the whole
rabbit hole of timer clamps is not as unreasonable as one might think, and
that doing so doesn't incur delays in any of those scenarios.

~~~
spion
Maybe it isn't that unreasonable. But personally I do like guarantees, and in
a stateful language I especially like guarantees about execution order.

If these guarantees can be offloaded to a library then thats really great, as
I don't have to keep them in mind anymore, freeing my brain to think about the
specific problem I'm trying to solve. One less thing to check for when
debugging too.

There is also the bonus feature that stack overflows wont happen, which
enables liberal use of recursive promise functions. (On the other hand, memory
usage might still explode, so its not quite that easy... :D)

~~~
lhorie
Those are good points. So far this approach hasn't been a problem w/ Mithril.
If it turns out to be a bad idea down the road though, it can always be
changed to conform w/ A+

------
enraged_camel
This is off-topic, but there seems to be this trend on HN (and probably
similar sites) where a post about Topic X will get a lot of comments/views,
and the following several days similar posts will appear on the front page, in
the format of "Why You Should Never Do X" or "A Better Way to Do X".

What I'm wondering is whether this is some sort of bias on my part (I tend to
notice them because my memory of the original post is still fresh), or whether
the subsequent stories are upvoted to the front-page because people want to
discuss the topic more. And from the author's perspective, whether they are
writing it to take advantage of the opportunity for increased page-views.

~~~
mattgreenrocks
It certainly happens. HN has a lot of self-promotion going on, and a lot of
people love to hop on bandwagons. Both reasons are compelling evidence for
consuming fewer blogs and more whitepapers; at least when it comes to the
practice of developing software.

~~~
gavinpc
> fewer blogs and more whitepapers

Is there a Hacker News for whitepapers? Because I've heard this comment a few
times now, and I'm ready to start.

------
tegeek
Promises are good solution to tackle call back problems. But once you spend a
little time with Functional Reactive Programming, You never want to go back to
any imperative solution.

Functional Reactive Programming eliminates every scenario where you need a
promise. FRP models "past, present & future" in a sense that you never need to
use any "promises" library again. It brings such a higher level abstraction to
your "time" dependent code, that you don't have to maintain state in your
code. BaconJS is one of the simplest FRP library which brings simplicity &
power to your JS projects.

[http://baconjs.github.io/](http://baconjs.github.io/)

~~~
domenicd
Scalar variables are a good solution to tackle register problems. But once you
spend a little time with Linked List Programming, You never want to go back to
any scalar solution.

Linked List Programming eliminates every scenario where you need a scalar. LLP
models "beginning, middle & end" in a sense that you never need to use any
"scalars" library again. It brings such a higher level abstraction to your
"space" dependent code, that you don't have to maintain state in your code.
JSClass's Linked List is one of the simplest LLP library which brings
simplicity & power to your JS projects.

[http://jsclass.jcoglan.com/linkedlist.html](http://jsclass.jcoglan.com/linkedlist.html)

------
andrus
The Promises/A+ assumption that you'd never want a Promise to return a Promise
is really shortsighted.

~~~
russellsprouts
Promises are really Monads for asynchronous results.

A Monad kind of wraps a certain value. For example, we might have a Promise
JSON from an API call. The bind method of a Monad (>>= in Haskell) takes a
JSON Promise, and a function (JSON -> Promise b for some b), and returns a
value of type Promise b. Promise.then is equivalent to bind.

In Haskell you might define them like this:

    
    
        data Promise msg val = Resolve val | Reject msg deriving Show
    
        instance Monad (Promise msg) where
            return = Resolve
    
            (>>=) :: Promise msg val -> (val -> Promise msg b) -> Promise msg b
            (Reject msg) >>= _ = Reject msg
            (Resolve val) >>= f = f val
    
    

They would be used like this

    
    
        newPromise = jsonPromise >>= transformJSON
    

In JavaScript, you would write

    
    
        var newPromise = jsonPromise.then(transformJSON)
    

However, the JavaScript case is slightly different. It is not as strict about
types. It is possible (and typical) for transformJSON to return a bare type,
rather than a Promise, while Haskell would require a Promise be returned every
time. It treats an unwrapped value the same as an already fulfilled promise
for that value. But automatically unboxing the promises that are returned
gives them the full power of Monads.

~~~
andrus
No, "automatically unboxing the promises that are returned" does not give
Promises "the full power of Monads", because you cannot represent a Promise
for a Promise for a value. Promises/A+ breaks parametricity.

------
dpweb
I'm not getting it. How is this?

    
    
      doSomething().then(function(result) {
        var results = [result];
        results.push(88);
        return results;
      }).then(function(results) {
        results.push(99);
        return results;
      }).then(function(results) {
        console.log(results.join(', ');
      });
    

More attractive/readable and superior to this?

    
    
      function doSomething(pushEight);
    
      function pushEight(res){
        res.push(88);
        pushNine(res);
      }
    
      function pushNine(res){
        res.push(99);
        showResults(res);  
      }
    
      function showResults(res){
        console.log(res.join(', '));
      }
    

In addition to the anonymous functions I can't reuse elsewhere? Serious
question, just asking..

~~~
pornel
Your example is nice because:

\- Your functions are coupled and cooperate on shared mutable state (you can't
pushNine and then pushTen). In large projects coupling like that starts to
hurt a lot, so you need more isolated functions and store intermediate state
somewhere - and closures are convenient for that, but don't nest too
gracefully.

\- You don't have error handling. Promises don't make it bulletproof, but at
least allow handling most errors in one place (so you don't need `if (err)
return callback(err)` in _every_ callback).

But IMHO the `.then()` syntax is just a temporary solution. Promises become
really awesomene with ES6 generators or ES7 async/await:

    
    
        async function doSomething(){ 
            showResults([await getEight(), await getNine()]) 
        }

~~~
dpweb
Dont care for the .then syntax. The async/await however I like alot.

------
malandrew
If anyone reading this is very familiar with the Bluebird promises library, it
would be awesome if you could write an in depth blog post on all the
optimization strategies used by bluebird to minimize the memory and cpu
overhead it achieves.

~~~
spion
Petka Antonov (the author) wrote this:

[https://github.com/petkaantonov/bluebird/wiki/Optimization-k...](https://github.com/petkaantonov/bluebird/wiki/Optimization-
killers)

Other than that, I think he also said similar rules of C++ and Java
optimization apply (avoid allocations a.k.a. creations of new objects, avoid
avoidable work in special cases, take advantage of cache locality etc)

------
KerrickStaley
If a Promise is rejected, what happens to the return value of that Promise's
.then() (assuming the onRejected handler doesn't throw)? Is it also rejected
(as the article seems to suggest)? Or is it fulfilled (as the spec indicates,
if I'm reading it correctly)?

~~~
domenicd
It's fulfilled. If you return from the rejection handler, instead of
rethrowing, then you have handled the error:

rejectedPromise.catch(e => { return 5; });

is similar to

try { throwError(); } catch (e) { return 5; }

------
auvrw
i'm at the beginning of a node project and need to choose a promises
implementation. it seems like when and q are the most often cited, but then
i've noticed someone mentioning this "bluebird" library. it seems like there
are a ton of these things. any advice on which one to pick? does it really
make a difference since they all conform to the A+ spec?

------
gcr
Whoa. We've shifted from the (simpler) continuation-passing style with
callbacks to _this_. Is this _really_ worth it? Honestly, I don't think I like
the whole idea of promises. What do we gain from this besides technical debt?

~~~
mkoryak
do the words "callback hell" mean anything to you?

~~~
gcr
As with all language constructs, it's not a problem if you're judicious.

~~~
domenicd
Who needs functions. Labelled gotos are not a problem if you're judicious.

