JavaScript Promises in Wicked Detail(mattgreer.org)
mattgreer.org
JavaScript Promises in Wicked Detail
http://mattgreer.org/articles/promises-in-wicked-detail/
9 comments
Whilst the concept of setImmediate is nice, the current implementation in IE isn't properly integrated with the rest of the event loop — resulting in broken behaviour when you use it in combination with setTimeout [1], DOM events, etc.
In contrast, using MutationObserver results in correct behaviour on all modern browsers and relatively minimal delays — between 0.002ms and 0.007ms according to an OS X only micro-benchmark I did last month [2].
And, yes, it would be great if we could call a builtin instead of hacking on top of MutationObserver, but it isn't that ugly:
[1] http://codeforhire.com/2013/09/21/setimmediate-and-messagech...
[2] https://gist.github.com/tav/9719011
In contrast, using MutationObserver results in correct behaviour on all modern browsers and relatively minimal delays — between 0.002ms and 0.007ms according to an OS X only micro-benchmark I did last month [2].
And, yes, it would be great if we could call a builtin instead of hacking on top of MutationObserver, but it isn't that ugly:
if MutationObserver
$div = root.document.createElement 'div'
observer = new MutationObserver tick
observer.observe $div, attributes: true
scheduleTick = ->
$div.setAttribute 'class', 'tick'
return
else
scheduleTick = ->
setTimeout tick, 0
return
In conclusion, I agree that a feature like setImmediate would be great. But given IE's broken implementation and a viable workaround in modern browsers, I see no need to rush it. I'd rather they focused on: new features like Object.observe; improving the performance of old features like Object.seal; and finalising some of the ES7 ideas like exposing the event loop![1] http://codeforhire.com/2013/09/21/setimmediate-and-messagech...
[2] https://gist.github.com/tav/9719011
This. Ran into this very issue when I was implementing promises in 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).
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).
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.
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)
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 asappromise.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)
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.
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
"... 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
>> 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):
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.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...
If its from another library, then that other library may be using its own scheduler and you will loose those 5ms either way...
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.
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.
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)
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)
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+
I'm not really disputing the time anything will take, rather that "A+ doesn't require that you use the browser's asynchronous scheduling mechanism" – it very much does, even though that scheduling mechanism might be to occasionally flush your own managed event queue instead of handing every callback to the browser.
> IMO the browser vendors should just give in and add setImmediate
Firefox has just shipped native promises, and you can use them to polyfill setImmediate instead ;)
http://kangax.github.io/es5-compat-table/es6/#Promise
Firefox has just shipped native promises, and you can use them to polyfill setImmediate instead ;)
http://kangax.github.io/es5-compat-table/es6/#Promise
Promises are being moved to native implementations, so the lack of setImmediate isn't really a problem anymore.
Here's the Firefox bug about implementing (or not) setImmediate:
https://bugzilla.mozilla.org/show_bug.cgi?id=686201
https://bugzilla.mozilla.org/show_bug.cgi?id=686201
setImmediate works with macrotasks; promises work with microtasks. These are very different. See e.g. https://github.com/YuzuJS/setImmediate#macrotasks-and-microt...
Also, note that yield/generators are entirely synchronous, and cannot be used as a scheduling mechanism of any sort.
Also, note that yield/generators are entirely synchronous, and cannot be used as a scheduling mechanism of any sort.
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.
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.
One reason is I think topics drop off too fast. I know there is a flame ramp-up algorithm or whatever it is called when a large number of comments is added. But I think that needs to be refined. If there a large number of longer and highly upvoted comment that thread should stay up higher.
Say just recently there was an interesting thread comparing Erlang and Go concurrency and thread safety. There were interesting in-depth comments about schedulers and whatnot. Seemingly a good number of upvotes, yet before the end of the day the story was gone.
So I think people do want to discuss things more and it needs to be fixed.
Well I guess another way is to embrace this and just re-post other blogs and reactions and then in the thread post links to previous comments (as in "wait, you probably want to read the story from yesterday before commenting here..."), kind of idea.
Say just recently there was an interesting thread comparing Erlang and Go concurrency and thread safety. There were interesting in-depth comments about schedulers and whatnot. Seemingly a good number of upvotes, yet before the end of the day the story was gone.
So I think people do want to discuss things more and it needs to be fixed.
Well I guess another way is to embrace this and just re-post other blogs and reactions and then in the thread post links to previous comments (as in "wait, you probably want to read the story from yesterday before commenting here..."), kind of idea.
Topics do trend, and the various camps will quickly rush to author and promote things that conform with their worldview. Users will search for correlating content to submit.
But to address something that is a personal peeve -
"And from the author's perspective, whether they are writing it to take advantage of the opportunity for increased page-views."
Much of the content that we see on here are people writing about topics that motivate them. If they saw a post that they think is wrong and they want to refute it, or if they think it is right and want to add supporting commentary, they author posts to support their position.
The majority of these posts have no ads, which is good because it is catering to a market that overwhelmingly runs ad blockers. There is close to zero retention or residual value for audiences like HN: people click to your post, maybe read it (if that), and for the overwhelming majority that is the entirety of the relationship with the author.
You don't shoot to the tops of the blogosphere. You don't rake in those fat Adsense dollars. You get a momentary period in the sun to evangelize some beliefs.
But to address something that is a personal peeve -
"And from the author's perspective, whether they are writing it to take advantage of the opportunity for increased page-views."
Much of the content that we see on here are people writing about topics that motivate them. If they saw a post that they think is wrong and they want to refute it, or if they think it is right and want to add supporting commentary, they author posts to support their position.
The majority of these posts have no ads, which is good because it is catering to a market that overwhelmingly runs ad blockers. There is close to zero retention or residual value for audiences like HN: people click to your post, maybe read it (if that), and for the overwhelming majority that is the entirety of the relationship with the author.
You don't shoot to the tops of the blogosphere. You don't rake in those fat Adsense dollars. You get a momentary period in the sun to evangelize some beliefs.
This has happened at HN for as long as I can remember. I generally like it, as I like the added depth and new viewpoints this tends to bring.
For what it's worth, I wrote this article on promises, but I didn't submit it today. I was surprised to see it show up here. So the trend is not always people trying to take advantage for their own benefit.
For what it's worth, I wrote this article on promises, but I didn't submit it today. I was surprised to see it show up here. So the trend is not always people trying to take advantage for their own benefit.
I'm looking forward to this. I'm currently forced to deal with this strange world of promise based CSP-style development and I'm not sure if I like it or not, but it certainly is interesting. And I can certainly see the charm of such a solution, I can ignore that certain calls can block and everything stays responsive.
Note that this is coming from someone who's usually fudging around somewhere deep in a server stack and who usually works on high-performance systems with careful control about threads, hot loops, boundaries between threads and such. Responsiveness isn't my world, but maybe I can steal some ideas here :)
Note that this is coming from someone who's usually fudging around somewhere deep in a server stack and who usually works on high-performance systems with careful control about threads, hot loops, boundaries between threads and such. Responsiveness isn't my world, but maybe I can steal some ideas here :)
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.
> 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.
Is there a Hacker News for whitepapers? Because I've heard this comment a few times now, and I'm ready to start.
So look out for co, fibers, async, etc. posts in the coming days. Promises are still my flavor of choice for most use cases.
[deleted]
It's called content marketing.
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/
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/
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
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
Promises aren't really imperative - they're just not suited to handling streams of events, only single values.
Also, isn't suggesting FRP in place of promises like suggesting streams instead of callbacks, or arrays of values instead of values?
Also, isn't suggesting FRP in place of promises like suggesting streams instead of callbacks, or arrays of values instead of values?
The Promises/A+ assumption that you'd never want a Promise to return a Promise is really shortsighted.
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:
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.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.
Promises are an ad hoc, informally-specified, bug-ridden, slow implementation of half of a monad.
I get a _lot_ of mileage out of being able to chain promises like that. If I really need to resolve a promise to a promise, I can resolve to an object wrapping the promise and then unwrap it on the receiving side.
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..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:
- 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()])
}Dont care for the .then syntax. The async/await however I like alot.
These snippets are not even remotely equal. Leave the former untouched and modify the latter to this and you have somewhat equal semantics:
function doSomething(null, pushEight);
function pushEight(err, res) {
if (err) return error(err);
try {
res.push(88);
pushNine(res);
}
catch (e) {
error(e);
}
}
function pushNine(err, res){
if (err) return error(err);
try {
res.push(99);
showResults(res);
}
catch (e) {
error(e);
}
}
function showResults(err, res) {
if (err) return error(err);
try {
console.log(res.join(', '));
}
catch (e) {
error(e);
}
}
function error(err) {
console.error(err.stack);
}
If you attach a `.catch(function(err){...})` at the end of the promise chain, then replace that function's body with the body of the error callback that just logs the trace.How about this slightly more realistic example?
https://gist.github.com/spion/33b3bf013cc9181171b9#example
https://gist.github.com/spion/33b3bf013cc9181171b9#example
The TL;DR:
* Callbacks passed to cb-based libraries may be called twice - promises are only resolved once, either fulfilled or rejected.
* Callback-based libraries may blow up if the callback throws - promise libraries shield the callback stack from "blowing up" (but do require slightly less careless management of resources at the call-stack site)
* Callbacks passed to cb-based libraries may be called before the current even tick completes, potentially creating the zalgo problem - callbacks attached to promises are always called after the currently executed function finishes.
* Callbacks passed to cb-based libraries may be called twice - promises are only resolved once, either fulfilled or rejected.
* Callback-based libraries may blow up if the callback throws - promise libraries shield the callback stack from "blowing up" (but do require slightly less careless management of resources at the call-stack site)
* Callbacks passed to cb-based libraries may be called before the current even tick completes, potentially creating the zalgo problem - callbacks attached to promises are always called after the currently executed function finishes.
You're making an argument about callback aggregation. That is just one of the features that promises enable. The seminal article on the topic is Domenic Denicola's "You're Missing the Point of Promises." http://domenic.me/2012/10/14/youre-missing-the-point-of-prom...
See slides #19-#28:
https://www.dartlang.org/slides/2013/06/dart-streams-are-the...
It's Dart instead of JS, but the idea is pretty much the same.
https://www.dartlang.org/slides/2013/06/dart-streams-are-the...
It's Dart instead of JS, but the idea is pretty much the same.
as someone else has already mentioned, you're not using the second (error-handling) argument to .then(). you might also check out the aosa on twisted (http://www.aosabook.org/en/twisted.html). twisted's Deferreds seem fairly similar to javascript promises as far as i can gather.
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.
Petka Antonov (the author) wrote this:
https://github.com/petkaantonov/bluebird/wiki/Optimization-k...
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)
https://github.com/petkaantonov/bluebird/wiki/Optimization-k...
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)
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)?
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; }
rejectedPromise.catch(e => { return 5; });
is similar to
try { throwError(); } catch (e) { return 5; }
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?
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?
Callbacks seem simpler in a lot of these examples because they aren't really showing the power of promises.
Promises allow you to chain much more complex flows of information together in a way that would result in a mess of spaghetticode if attempted using callbacks.
Promises allow you to chain much more complex flows of information together in a way that would result in a mess of spaghetticode if attempted using callbacks.
I suggest watching:
Forbes Lindesay: Promises and Generators: control flow utopia -- JSConf EU 2013 https://www.youtube.com/watch?v=qbKWsbJ76-s
Forbes Lindesay: Promises and Generators: control flow utopia -- JSConf EU 2013 https://www.youtube.com/watch?v=qbKWsbJ76-s
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.