JavaScript Promises in Wicked Detail
mattgreer.org
mattgreer.org
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.
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...
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).
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)
"... 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
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 its from another library, then that other library may be using its own scheduler and you will loose those 5ms either way...
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.
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)
Firefox has just shipped native promises, and you can use them to polyfill setImmediate instead ;)
Also, note that yield/generators are entirely synchronous, and cannot be used as a scheduling mechanism of any sort.
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.
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.
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.
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.
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 :)
Is there a Hacker News for whitepapers? Because I've heard this comment a few times now, and I'm ready to start.
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.
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.
Also, isn't suggesting FRP in place of promises like suggesting streams instead of callbacks, or arrays of values instead of values?
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. 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 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()])
} 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.* 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.
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://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)
rejectedPromise.catch(e => { return 5; });
is similar to
try { throwError(); } catch (e) { return 5; }
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.
Forbes Lindesay: Promises and Generators: control flow utopia -- JSConf EU 2013 https://www.youtube.com/watch?v=qbKWsbJ76-s