Managing Node.js Callback Hell with Promises, Generators and Other Approaches
strongloop.com
strongloop.com
Bluebird is not only so fast that it's almost on par with callbacks[1], but also innovates on the ability to filter on catching different error types[2], support for generators[3], context binding[4] among other things.
[1]: https://github.com/petkaantonov/bluebird/blob/master/benchma...
[2]: https://github.com/petkaantonov/bluebird/blob/master/API.md#...
[3]: https://github.com/petkaantonov/bluebird/blob/master/API.md#...
[4]: https://github.com/petkaantonov/bluebird/blob/master/API.md#...
The stack traces and error management is amazing and everything is very fast.
Great to see progress being made, I guess. :(
I just found this after some cursory Googling and haven't tried it at all, but it appears to be a compatibility layer for Q on top of Bluebird (written by the author of Bluebird): https://gist.github.com/petkaantonov/8363789
It's interesting--I'd considered myself pretty OO in my style, pass a message, hope for the best, but the promises stuff is kind of taking that to its natural conclusion.
[1]: https://github.com/petkaantonov/bluebird/blob/master/benchma...
It is something like this:
---
callback(find_largest(get_stats(read_files(dir)))
---1 line.
If an error happens, an exception should be raised.
The fact that there are multiple such requests happening or that some of those operations have to wait for a select or epoll loop to return, should be handled by an underlying framework. The fact that even with promises you have to write 27 lines of code to do that, should be making you worried.
With promises (Bluebird) this sounds like very little code in practice, assuming promisifyAll has been called the FS module:
- One line to read all files in a directory
- Map them with .map to a .stat call on them, returning array of promises.
- Reduce them to the largest element.
Something like:
var fs = Promise.promisifyAll(require("fs"));
var largest = fs.readdirAsync("/your/file/path").map(function(file){
return fs.statAsync(file);
}).reduce(function(x,y){
return (x.size > y.size) ? x : y;
});
With arrow syntax: var largest = fs.readdirAsync("/your/file/path").
map((file) => fs.statAsync(file)).
reduce((x,y) => (x.size > y.size) ? x : y);
Alternatively, with Generators using Bluebird promises var largest = Promise.coroutine(function *largest(dirname){
var files = yield fs.readdirAsync(dirname);
var sizes = yield files.map(name => fs.statAsync(name));
return files.reduce((x,y) => x.size > y.size ? x : y);
});
If one error occurs, the promise rejects. VERY far from 27 lines of code. Not to mention you can of course compose these in one line if you extract them to methods like in your code: var largest = fs.readdirAsync("/your/file/path").map(fs.statAsync).reduce(largest);Was that addressed to strongloop.com? They are the ones that posted an example with promises using 27+/-whitespace lines of code and the initial problem algorithm only had 4 lines.
Sounds like you should write a blog post and not them about using promises.
I don't use Node.js on daily basis and am not familiar with promises so just commented as an outsider based on the article.
Your last one line looks fantastic and makes it clear what is going on. Any reason you can think of why that wasn't the "obvious" solution to the original posters of the article?
Oh wow, this is my fault, I didn't read what they did there. I have __no idea__ why it took them 27 lines to implement this. It's rather trivial with promises.
- They have a .then followed by a .map there causing nesting for no reason whatsoever.
- They also get the file name, but handle this very poorly (with an array), where they could just do it as a part of the .map (the .all and .then are completely redundant) there
- They assume the possibility of directories, I didn't. That .filter is justified but that's the only thing one might have to add here.
- Generally, it's poor promise code.
I guess that promises are just not their tools of choice and they just used them _for that post_.
For better or worse, Node's best practice seems to be to propagate errors via the err parameter in each callback. E.g. `function myCallback(err, data)` where data represents the result of a successful computation.
One could reasonably debate whether this was the best possible design. Some people like exceptions, for one thing. For another, JS's dynamic typing, combined with multiple layers of error propagation, could lead to mishandling of the errors--you expect your callback to be passed error A, but it actually gets error B from elsewhere in the callback chain.
But this pattern is ingrained by now. If people started deviating from the pattern and using exceptions, you'd end up with an even worse monstrosity. Every bit of calling code would have to handle both styles of errors.
function g() {
return somePromise.then(function() {
setTimeout(function() {
throw new Error();
}, 500);
});
}
g(); // crash
But at that point, you're not using promises anymore so all bets are off.However if you use a promise-based timeout too:
function f() {
return somePromise.then(function() {
return Q.delay(1000).then(function() {
throw new Error("Oops");
});
});
}
f().catch(function(e) {
console.log(e.message); // logs Ooops.
});
No problem bubbling that.(However, they do leave async exceptions from callbacks alone - only domains can handle those)
You wouldn't even need that one callback, just:
return find_largest(get_stats(read_files(dir)));
Exceptions are propagated as you would expect from synchronous code, despite being async under the hood.
read_files(dir).then(get_stats).then(find_largest).then(callback, errback)
Any exceptions in those functions would propagated to the "errback". Not perfect but much better than the examples in the article would suggest.Once the "callback hell" example was re-written with named functions, it is not much different than the generator example and depending on how you are testing, easier to test because it separates out IO from logic (Which you could still do in the generator example).
One thing callback style code has begun to make me think about is the nesting/IO complexity of the functions a I write. When you nest, the IO/event boundary is visible in the code (by the indentation). As you continue to nest, you can start to get a clear idea of the amount of IO being done and where a particular function may be doing too much. Anyhow, most of this is preference anyway :)
Dir.glob('*').select{|f| File.file?(f) }.max_by{|f| File.size(f) }
Plus, the error semantics of this code are sane: if something goes wrong I get an exception I can rescue. If there were no files in the directory, I get nil.Statting a bunch of files is rarely so costly that parallelism is warranted, unless it is impressed upon you as in Node. The cost in readability and dreaming up generators and promises to solve such a mundane problem is likely much worse. Before you charge headlong into building your next webapp with Node, consider this article food for thought.
The pain and constraints introduced by this forced use of async sure has spawned a lot of wheels of all sizes, colours, and compositions.
I hit Go just because it's the most Algol-ish of the sane languages. There's several other options where you just write the code and the compiler and runtime do the async for you.
fs.readdirSync(dir).map(function (f) {
return {
name: f,
stat: fs.statSync(path.join(dir, f))
}
}).sort(function (a, b) {
return b.stat.size - a.stat.size;
});
Not going to win any code golf tournaments, but that's JavaScript for you. It was never designed to do this sort of thing. Its strengths lie elsewhere. Dir.glob('*').filter(f => File.isFile(f)).maxBy(f => File.size(f));
Asynchronicity isn't the issue there, the issue is that most JS / node libraries are minimal, not optimized for ergonomic (and of course the verbose lambda syntax)The best I could find at the time is IcedCoffeescript, which sort of replaces the whole existing coffee script tooling, so now you are dealing with custom tooling to install for each team member as well.
I find it sad that years later it looks like what IcedCoffeescript did is still the cleanest looking solution to the problem. It feels like the evented model is maybe great for smaller things, but horrible for building and reasoning about in larger systems on larger teams.
getUser = (user) ->
_("https://npmjs.org/~#{user}")
.get()
.extractLinks()
.filter( -> /package/.test(arguments[0]) )
.map( -> "https://npmjs.org#{arguments[0]}" )
.log()
getUser("foo")
where `get` is THE jQuery get for Ajax requests returning a promise and is chained with the rest of the functions in a pipeline fashion. I've found that these techniques can work very well for form validation, as [you can check here](http://www.vittoriozaccaria.net/chained/demo.html) — Firefox required.Edit: I wrote in markdown but it seems that only verbatim is supported. Edit2: some clarification
Lightweight threads first remove the problem, and then let you choose the most appropriate programming style for your domain. See https://news.ycombinator.com/item?id=7179144
fs = require 'fs'
async = require 'async'
path = require 'path'
module.exports = (dir, cb) ->
err, files = fs.readdir! dir
paths = (path.join dir, file for file in files)
er, stats = async.map! paths, fs.stat
largest = 0
for stat, i in stats
if stat.isFile and stat.size > largest
fname = files[i]
largest = stat.size
cb fname
I use ToffeeScript for everything now, since it makes things much easier to read.