I disagree. Coding directly against system calls is much harder in C, because you don't have the advantage of closures and flexible typing. But, that aside, node is intended to be a very low-level library that facilitates higher-level extensions and abstractions in userland. It is more like C than it is like Python, and that is by design.
Node is JavaScript on the server. It is not JavaScript-like, or Compiles-to-JavaScript, or Erlang-with-semicolons-and-braces on the server. We don't mess around with the language runtime, we take it mostly as-is, and there is a huge benefit to doing that.
When and if V8 implements generators (as they are likely to do somewhat soon), then I expect we'll see a lot of experimentation in this area in userland modules. They'll have to be run with a --harmony_generators flag, most likely, but they won't need to be compiled or do scary bad-touch things with threads and stacks.
When the area has been explored a bit in those userland modules, and one or a few of them are popular and good and intuitive to use, and V8 moves generators out from behind a flag, and they're fast enough to be used in node without introducing performance regressions, then we'll investigate adding something like this to node-core.
Part of the reason why you meet such backlash from people in the Node community when you complain about "callback hell" is that the model is very simple, and it really is not as bad as it looks at first. JavaScript's bulky "function" keyword does make CPS quite a bit uglier than it is in Scheme, but it's a very reasonable approach to the problem which is extremely extensible.
The crappiest part in my opinion is doing `if (er) return cb(er)` all the damn time. Domains make that a little bit easier, but you're just trading one bit of boilerplate for another, so I don't know. Generators are probably the ideal approach to that problem, but I'm personally not sure they're worth the complexity cost they introduce. I am often wrong, and try to be quick to admit it. We'll see how they change the shape of things once they're a real thing and not just an idea.
In the meantime, use named functions. Use the "async" utility. Use Stream interfaces and .pip() them to one another. And most of all, Don't write big apps! Write small modules that each do one thing, and assemble them into other modules that do a bigger thing. You can't get into callback hell if you don't go there.
Stuff like "hey @nodejs people lets make a website called http://fibersarestupid.com in which we provide education on how to use callbacks and streams" certainly doesn't help, it just makes node as a community look childish, maybe the site should be called iDontUnderstandFibersThereforeIDismissThem.com... come on.
That is heart of my complaint. And it's why I made an analogy to system calls, because there you end up doing the same thing -- manually propagating error codes.
> It is more like C than it is like Python, and that is by design.
I agree, which is why the original article simply makes no sense when it presents Node as a competitor to Ruby. They aren't really comparable.
I'm convinced the only way you get this kind of consensus is by building a feature into your language's standard library. This is one big reason that languages with robust "batteries included" standard libraries have been successful. It makes it so much easier to interoperate when everyone can just assume the same abstractions are always available.
I don't think there really needs to be a fibers based ecosystem. Isn't it best if modules don't rely on fibers such that they can be reused in either context?
If you prefer that the language does that for you, then that's great. There's enough viable options out there to fit anyone's preferred asynchronous style. Node exposes a lot of the low level elements of asynchronous programming (much like the low level programming you could do in C). You can stick with that, or use a flow control library, or employ any other number of viable strategies to avoid deeply nested callback soup. You get to pick the level of abstraction you work on, and not everyone wants to work on the highest levels of abstraction (where the language or framework handles everything and you just write synchronous style code).
The node community could probably do a better job of directing people to good documentation on those different levels of abstraction (it's a bit silly to assume someone will know to use streams, or monads to clean up the style of their code). When node first started to gain traction, it felt like almost everyone wrote a flow control library. Now the problem is solved in almost every way imaginable. The problem is that new users don't know where to look to find those 3rd party solutions, or how to evaluate the tradeoffs.
You're right to find it maddening. A lot of other people did as well. It doesn't need to be maddening though, and while you have a valid point, it's not especially relevant given the number of possible solutions (you just have to know how to find the style you're after).
The Node ecosystem clearly has no consensus on this point. So you spend a lot of time gluing pieces together.
And I do know how to find the style I'm after. At the moment I'm doing a lot with Q, and I have also used node-fibers. But in either case, you end up doing a lot of plumbing, because you end up depending on other people who chose different (or non-existent) high-level flow control abstractions.
Edit: forget it. Someone else mentioned it in another comment: https://github.com/kriskowal/q/
But as far as I know, it's still not competitive. Which makes sense, since the kernel offers far stronger separation guarantees.
If you like to have a sync model on node, fine, add a layer like threads-a-gogo and you are ready. By doing so, you give up some of the lightweight-ness and will have increased memory consumption. It is your choice and it depends on your use case. If you expect just a few hundred connections or you have lots of memory, you don't need to think about node.
Raising an exception and dealing with it in the same lexical scope sure is nice.
The author lists some examples, another is: http://www.gevent.org/
Anyway, the call for a sync model is nearly as old as node is and I agree that a solid thread implementation should be part of node for those who like to code this way.
And it is not a call for a sync model. It is a call for a sane API.
About a benchmark, I have none, but it is the nature of threading. Threads need to store and switch contexts. Even if it is just a few 100KBs with 100000 connections this easily multiplies to a gig of additional memory use. In the real world this is often a few MBs per thread making that scale even impossible for a mid class server.
Here's a paper on using userland threads to get the API advantages of threading, with the scaling advantages of event driven programming: http://static.usenix.org/events/hotos03/tech/vonbehren.html
Basically it means you can't rely on exceptions at all, so you're back in a "always check for the error code" world.
https://github.com/kriskowal/q/ https://github.com/cujojs/when
But until the Node community can agree on a standard for this, library writers can't make their APIs dependent on these techniques, so we're stuck at the lowest-common-denominator.
And even if everybody was using Q, it's still an awful lot of boilerplate compared to saying the same thing in a language with coroutines.
And there are painful design choices that make it unlikely everyone will ever agree on a promise library. For example: Q guarantees that promises will resolve on a different stack than where they were created. This is nice, it helps you reason about the code. But there are places where you absolutely need to resolve a promise on the same stack (Node's IO handlers, or parts of the window.openDatabase API), and dealing with the edge cases is really gross.
By the way, Q can easily wrap node-style callbacks in a single line.
We actually got it in the 90s when Tcl had this, only better...
The whole performance argument is completely beside the point. Yes, we get it, in-process asynchronous parallelism is where its at. But it simply doesn't follow that we need to live in inversion-of-control hell.
Even the simplest node program ends up drowning in error-handling code, because there's no way to automatically propagate errors back to the right place. Every function needs to handle error conditions immediately, because you can't throw an error up the stack, because your stack is completely meaningless.
This is a huge step backward. It feels like coding directly against system calls in C.
It doesn't need to be this way. There's no reason your language runtime can't be smart enough to switch contexts automatically, so that your code gets to be "blocking" even though the actual OS-level process is never blocked. Erlang does this, Stackless Python does this, any Lisp with good old call-with-current-continuation does this.