Hacker News new | ask | show | jobs
by reqres 3481 days ago
> Which leads to the 'callback hell' that we all know and hate. Part of the bill-of-goods we accept when using Node is that in exchange better time and space characteristics, we lose the thread as an abstraction.

Perhaps my head has been stuck in javascript/node land for too long but I think accusations about javascript producing callback hell now seem a bit disingenuous even for relative novices to the language.

It's 2016 and there are many well documented and widely adopted solutions arising from external libraries and developments in ECMAScript. Thanks to transpilers like Babel/Typescript we can even shoehorn these new ECMAScript features into older browsers.

8 comments

> It's 2016 and there are many well documented and widely adopted solutions arising from external libraries and developments in ECMAScript.

"Solutions" is a strong word for what's available in JavaScript. Promise hell isn't better than callback hell, it's just horrible in slightly different situations. To make matters worse, it seems that many JS libraries have just wrapped the old callbacks in promises, meaning that we end up using promises in situations where a callback would actually be easier (because that's how it was originally written).

None of this comes close to the ease of threads in Erlang.

With async/await (currently available and pretty stable in node, you just need the --harmony flag. You can also compile to it pretty effortlessly) you basically get the exact same code style you get in Erlang/Haskell/Other threaded models but with delicious event loop goodness.
Not only is Erlang's and Haskell's threading model unlike async/await, but Erlang's threading model is unlike Haskell's. So I'm just not sure what you're taking about.
I meant the coding style, not the threading model. Like the author talks about, one of the nice things about the threaded languages is that you basically just write normal, synchronous looking code and get asynchronous results. async/await lets you do basically the same thing.
I never had callback hell even before modern Javascript times. I simply used named functions instead of inlining everything. Nesting level: 1, maximum 2 if I felt it was okay. And modules, modularization is key or it gets too complex.

So the lexical structure of my code was linear - while the runtime structure was nested at arbitrary levels. There never was a reason to represent the nested runtime structure in the written code.

I also didn't attempt to use node.js for things it wasn't made for, like compute-intensive tasks or implementing business logic. The good old chat server for ten thousand people was an often used example for node.js programming for a reason - lots of I/O, little processing.

Note: I don't write code like that any more in ES 2015. I also don't use classes, prototype, this, bind, apply - only functions and (lexical) scope (with an eye on capturing only as much scope as I need). Which is the opposite of the above described method where lexical scope was not usable, but with the methods available now the code still is "flat", so that's why I switched.

That sounds like its own form of hell to me. Ideally, a lexical structure helps visualize and understand the runtime structure. Anything that obfuscates that is a recipe for disaster.
Runtime structure can be arbitrarily nested, how do you want to show that in code structure? That makes no sense. You presume a static structure of who calls whom. It also isn't very flexible (refactoring, implementing change requests).

The key was of course to come up with great modularization, of course you would not want to do that with "flat code", the complexity of what function is where would be (or would have been, since I no longer need to write in that style) overwhelming.

I think that is part of the point. You want to do things that make it obvious when the runtime structure has gotten arbitrarily nested. Closure callbacks actually help there, since they make it someone visible and easy to "smell."

That is, if you have the same nesting at runtime, but it is just somewhat obscured by the naming style that you did, that sounds problematic to me. Ideally, you find structural ways to get rid of that nesting. (I said elsewhere that I'm a huge fan of first class queues. There are other options. Callbacks are one. And realistically, what you describe is an option, too. None of them are intrinsically bad.)

The point of the quoted snipped seems more aimed at the "we lose the thread as an abstraction" part.
Good. It's good to lose the stateful thread as an abstraction.

In the article we have someone promoting the use of a functional language on a functional programming site promoting the use of thread-lexical mutable variables to store state across procedure calls. The alternative the author dismisses is to swallow a return as an input. Something's not quite right here.

It's also a rather convoluted set of examples.

If you're doing something in Node that takes nontrivial processing time (that doesn't just involve waiting for I/O), you're Doing It Wrong (tm).

Since the premise of "callback hell" is outdated, the only remaining point is that if you perform complex/time consuming calculations in NodeJS, it slows down.

If you want the most speed, or if you're going to be doing something like a Fibonacci calculation, you should write that (microservice) in Go instead. It's faster from the start (by 2x at least, probably a lot more if you're CPU bound), and GoRoutines can be spread across multiple threads, so a multi-CPU host can take advantage of all of its CPUs.

Oh, and the per-GoRoutine overhead is only about 4K on a Linux host, last I checked. True that Haskell has a smaller overhead per thread, but considering the speed advantage, I think the Go server would still win big on latency for the number of threads that did actually fit in memory.

But if you're going to run to Go, Haskell, or something else to write micro-services to handle everything that Node doesn't do well, why not just ditch Node and write everything in that other language, since languages like Go, Haskell, Elixir, and Erlang are better and faster than Node at the things Node is good at as well?
But they aren't always. Go might, but I'd be less sure about Haskell in a fair test and Elixir/Erlang (as much as I love them and prefer them to everything else) are languages with strong benefits and strong drawbacks. Node actually does have some advantages.
Would you mind sharing some of those advantages of Node, and disadvantages of Elixir/Erlang? It's hard to have a meaningful discussion around vague assertions.
A key advantage that Node has? NPM with 350,000 packages.

Granted they aren't all top quality packages, but thousands are.

I was working on one project that used ZeroMQ, as a random example. There are excellent packages for Node, including a well maintained version for the newest 4.0 branch.

For Elixir, when a team wanted to interface using ZeroMQ, the best they could find was a "work in progress, not ready for production use" build that was a partial re-implementation of ZeroMQ 3.1, and that specifically lacked the elliptic encryption feature that we were using. The more mature ports to Erlang similarly doesn't support the encryption we were using. [1] There's one native binding that seems to only support 3.1 (at most) as well [2], but it's something you need to build and configure, as opposed to "npm install zeromq --save", or better yet, "yarn add zeromq", and then your project will work on any system without any complex build rules to get it working.

That's just one package that I tried to use, and it's a popular network message queue package (two full ports to native Erlang!). If I had tried to do something more obscure I'm sure I would have had even more problems.

Another key advantage is that there are probably 10x the number of developers ready to hit the ground running on a Node project than there are Go developers, or 50x as many as Elixir or Erlang developers. I don't know if you've tried to do much hiring, but it's hard enough finding developers for a language that's popular. (And no, I'm not counting "front end" JavaScript developers; if I were, I would have said 100x or more.) If you're hiring in an area that isn't highly tech focused, you might not be able to hire a single developer with experience.

[1] https://github.com/zeromq/ezmq/issues/31 and https://github.com/chovencorp/chumak (supports only the "NULL" security framework -- there's a note lower down on the page that Curve isn't supported)

[2] https://github.com/zeromq/erlzmq2

Node.js's NPM packages are notoriously dumb. How many of them are one-liners that only exist to compensate for Javascript's lacking standard library? How many of them are complete rewrites of other Node.js packages that do exactly the same thing?

Why would an Elixir programmer want to interface with ZeroMQ, when there are far better options for such communications built into the language itself? Node HAS to use ZeroMQ because it lacks the intrinsic ability to handle many problem spaces. Elixir and Erlang don't suffer from those shortcomings. In fact, they were designed to solve those types of problems. I've replaced ZeroMQ countless times when replacing Node code with Elixir code, and the end result has always been an amazing improvement in speed, stability, and scalability; not to mention code maintainability.

A lot of Node programmers tout npm as a great package manager, but how is it any better than gem, pip, and all the other package managers that it took its cues from? How is typing "npm install zeromq --save" any better or easier than typing "mix deps.get," "gem install zeromq," or "pip install zeromq?" Could it be that Node programmers are simply unaware than tools like npm have existed in other languages for years?

Finally, I'm sure there are 100X the number of Node programmers out there for every Erlang or Elixir programmer, but, when you need speed, stability, and scalability over trendiness, you really only need to have one.

Though, to be honest, most Erlang/Elixir devs would never think of using something like ZeroMQ as Erlang has more powerful and built in alternatives, or something like the Elixir Phoenix web framework that has built in much more powerful functionality. That seems to be an aspect of knowing the tools, easy to miss if you are new to the Erlang ecosystem. :-)
Async/await, promises. I don't think 'callback hell' is not a problem anymore, not even to newcomers (with the right material).
Unfortunately there isn't much standardization amongst popular npm modules. So a project can end up using callbacks, promises, event emitters, and the benefits of stitching together previous work rapidly disappears.
Indeed, node is likely to get native support for async await in the next couple of months (The version of V8 is Chrome 55, which is currently in beta supports them).
It's also somewhat disingenuous to talk about how Node can't take advantage of additional CPU cores when Node is basically designed to run multiple, identical processes which each have their own event loop. Most production Node deployments will do this and have a process supervisor that restarts failed processes and shares a single socket among all processes.

Running performance tests with a single Node process doesn't feel like it's giving Node a fair chance to perform up to the capabilities of the machine.

That's a hack. Not "the way Node is designed". Specially because Node didn't design much, they just took V8 with its own limitations and ran with it. While those limitations are acceptable for V8's original domain they are not for server applications.