Fixing the callback spaghetti in node.js (github.com)

222 points by koush ↗ HN
node.js is a fantastic platform, but it suffers from your code quickly turning into callback spaghetti and a mess of functions. I modified the V8 parser to support the await keyword that allows C# to handle this quite elegantly.

108 comments

[ 3.4 ms ] story [ 218 ms ] thread
This is very elegant. Just a couple of questions -

Using await - is the program flow suspended until the corresponding function returns , or does await keyword act more like a 'pause and continue' mechanism.

It exits the function, and resumes it when the callback completes. So other requests can be processed while the async function is (a)waiting.
(comment deleted)
How does this compare to http://chris6f.com/synchronous-nodejs ?
Not sure to be honest! I am still very new the to scene so I am unaware of this project. At first glance, we seem to have different approaches towards the same goal though.
That project requires fibers, in the first place, and doesn't seem nearly as elegant.
I've been wanting to implement something like this for a while! Great job. It looks elegant and basically pushes the continuations into the background.
The proper fix is to introduce coroutines.
That's exactly how this was done. I'm going to implement "yield" as well when I have some time. They both just leverage coroutines.
node-fibers is one attempt to put coroutines into node. I like it because it uses libcoro, which is nice and tight and uses ucontext, and because it works as a module without forking the core of node. Once you've 'fiberized' the server modules to start each request in its own fiber and implemented a call-with-current-continuation on Function.prototype, we're pretty much at the same place you've arrived but without new keywords.
Ah I missed this. I once tried to implement coroutines in v8 but v8 makes use of getspecific and setspecific pthread methods which makes it more complicated.
Very cool, but it's too bad you have to deal with all that to begin with.

I've been using LuaJIT embedded in Nginx (LuaNginxModule). Lua supports coroutines, so a function can just yield. Here's a brief example:

    -- Query a database using an http backend.
    -- Yields and handles other requests until the reply is complete.
    local record = util.getUserRecord(userId)

    -- Send some text to the client. Yields control while
    -- the actual transfer is in progress.
    ngx.print( "Result=" )
    
    -- Send the result, encoded as JSON, to the client
    -- Again, this call doesn't block the server.
    ngx.print( cjson.encode(result) )
    
With code like the above I can easily handle into the thousands of concurrent connections per second on the lowest end Linode VPS node available, with barely any load on the box -- and I'm told it should be able to handle 40k+ connections per second, if I were to do any tuning. Oh, and I have only 512Mb of RAM, which it doesn't even get close to under load. And the longest request took less than 500ms at high load.

I've been using OpenResty [1] which has the Lua module and a bunch of others all configured together. Works great, and I can't complain about the performance.

Someday I'm sure I'll hand the maintenance of this off, and then I might regret not using one of the "popular" frameworks. But the code is SO straightforward using this stack -- and what I'm using it for is so simple -- I think not.

[1] http://openresty.org/

IIRC, the original plan for Node involved coroutines (maybe even Lua?). But, they were scrapped because they introduced a lot of the same problems as full threading. You never know if the state of the world is going to change in ways unrelated to the function you are about to call because some deep-nested subroutine might yield.
Can you give an example of this?
The real reason will be that V8 does not support co-routines.
I think corysama is referring to Ryan Dahl experimenting with Lua (as well as C and Haskell) before settling on JS/V8.

I had several failed private projects doing the same on C, Lua, and Haskell. Haskell is pretty ideal but I’m not smart enough to hack the GHC. Lua is less ideal but a lovely language - I did not like that it already had a lot of libraries written with blocking code. No matter what I did, someone was going to load an existing blocking Lua library in and ruin it. C has similar problems as Lua and is not as widely accessible. There is room for a Node.js-like libc at some point – I would like to work on that some day.

V8 came out around the same time I was working on these things and I had a rather sudden epiphany that JavaScript was actually the perfect language for what I wanted: single threaded, without preconceived notions of “server-side” I/O, without existing libraries.

http://bostinnovation.com/2011/01/31/node-js-interview-4-que...

The threading model in Haskell (GHC) would be ideal, as it's essentially abstracted away from the developer. There's certainly the potential for a performance advantage there as well as easier to maintain code. The advantage Node.js has, is that the vast majority of web developers already know javascript. It doesn't need to be the "perfect" server-side implementation, it just needs to be good enough. It's success is due to it's easy of accessibility, something he would have missed out on had he gone the Haskell path.
Its not just potential - all the major Haskell web frameworks can handle more concurrent requests than node.js and scale to multi-core. http://www.yesodweb.com/blog/2011/03/preliminary-warp-cross-...

I don't think node.js is good enough because you have to deal with the issue being discussed here. In Haskell, as you said, you just write normal code with no worries about mutable state messing with your thread.

Isn't that sort of the point? If you use node, you never know if the state of the world is going to change in ways unrelated to the callback you are about to pass, because you expect the world to change between now and the invocation of the callback.
You never know if the state of the world is going to change

It's the same situation when using callbacks.

The difference is that, for the duration of the callback, you know the state will only change in ways directly related to the functions you are calling. Between callbacks, anything could happen. But, at least you have an island of sanity within the callback. With coroutines however, any function you call might block for some obscure reason (logging some debug info to a file, for example). That means you can only be sure that the world won't move behind your back as long as you don't call any functions :)

I don't have a lot of experience in this area. I'm just reporting back what I remember hearing.

The difference is that, for the duration of the callback, you know the state will only change in ways directly related to the functions you are calling.

That is true for a single threaded reactor, but in practice I haven't found it to be an especially useful property because naturally that doesn't include external state (i.e. the database).

Getting atomicity right in evented-code has in fact often been a hairier issue for me than doing the same with co-routines or threads, because when you're not allowed to block ever then you quickly find yourself in a situation where you need a retry-mechanism.

Such codebases then tend to quickly converge towards the actor-pattern (tied together by queues) which, ironically, could be had much easier by starting out with co-routines in first place.

Ahh, but here's the deal: The way the Nginx Lua module is written, you store everything in local variables. Those stay consistent across yields, and are tied to a single invocation of the Lua function.

So except for things you should EXPECT to change (like the state of a database you're querying) between calls, the "world" you (as a programmer) should care about stays perfectly consistent.

Unless I'm not understanding what you're asking -- is there some situation that I haven't encountered where the state of something that isn't a local variable matters?

The problem wasn't intrinsic to coroutines as much as it was the way they made the event loop reentrant which is a horribly complex thing to track. In most cases like that you'd either suspend the loop or have coroutine local event loops.

Of course people like to point at the overhead of native threads and assume coroutines have similar overhead, which is total bunk. Ironically, event sources in node use a stack-like tracking element which brings back a similar sort of overhead you see in coroutines in Lua, for example.

I doubt we'll see node take another shot at coroutines but that's okay. Node will do fine without coroutines but it will come at the cost of making certain types of code a little less natural (same as eventide code in threads becomes unnatural).

This kind of stuff always makes me doubt why particular stacks are the most popular.

Wondering, is the OpenResty solution otherwise single-threaded-with-an-eventloop much like node?

Well, you can configure how many processes nginx runs with at startup, but otherwise yes it is an event loop based system. Lua is just a module like anything else in Nginx, and uses the Nginx event loop.
Ah right, I see.

From the nginx wiki:

> Unlike Apache's mod_lua and Lighttpd's mod_magnet, Lua code written atop this module can be 100% non-blocking on network traffic as long as you use the ngx.location.capture or ngx.location.capture_multi interfaces to let the nginx core do all your requests to mysql, postgresql, memcached, redis, upstream http web services, and etc etc etc (see HttpDrizzleModule, ngx_postgres, HttpMemcModule, HttpRedis2Module and HttpProxyModule modules for details).

Is that talk-via-nginx-commands thing cumbersome in practice?

No, it's pretty easy. An example:

    local doc = ngx.location.capture(  "/couchdb/hamster/"..uid );
This queries the local CouchDB for a particular user record, and stores it in a local variable.
theres this great thing called npmjs.org that is a place for stuff like this if I am not mistaken
Perhaps it would be interesting to see how it was done in Twisted: http://twistedmatrix.com/documents/current/api/twisted.inter...

We'd like to think that the C# guys were looking our way when they came up with async/await, but there's no proof. :3

I love the way Twisted does it. Completely part of the way the language is supposed to work. It's a work of art IMO.
Isn't this the best way so far to do these things? It turns asynchronous code into synchronous. I seem to remember Guido recommending this approach too.
C#/.Net is planning on async/await features for its .Net 5 release: http://blogs.msdn.com/b/csharpfaq/archive/2010/10/28/async.a...
and in current release F# (current being pre-Build 2.0, i.e. RTM for over a year, tho I can't comment on whether you'd really like to have VS Ultimate to make it work)

http://tomasp.net/blog/csharp-fsharp-async-intro.aspx

http://news.ycombinator.com/item?id=2999260

http://bvanderveen.com/a/owin-buffering-async/

http://devtalk.net/csharp/async-await-and-c-vnext/

http://msmvps.com/blogs/jon_skeet/archive/2010/10/29/initial...

(read comment)

The current release is included in VS Professional, Ultimate isn't required. It seems like the last recent installer for VS Shell is the CTP, so expect some difficulties if you want to go the free route on windows, although it's possible that SharpDevelop integrates with the latest source drop.

Anyway, it shouldn't be too difficult to build the compiler yourself and set up a basic emacs/vim + mono environment for linux.

koush, nice! I use your code every day when I reboot my phone, but I appreciate this useful extension to node almost as much. :)
Very nice. I've also been using the async node.js module which is quite handy for some other patterns. Of course, having direct support directly in node would be the best. Can't wait for the yield keyword!
It's a lot of syntactic sugar for one very specific type of monad. Why not just patch the language to allow easy interoperation with any monad?

For example, F#'s let! binding works with any monad not just async.

I'm not sure monads work so well in languages without type-checking. That's certainly my experience.
There was some discussion of adding this to CoffeeScript. The issues on GitHub seem stalled. Would really love to see it happen soon!
My understanding is that the CoffeeScript status is won't fix because the transformation violates some constraints about the complexity of the mapping that CoffeeScript values.
Changing JS is just not a great idea and I think Node.js has been wise to avoid it.

If you're going to innovate, then design a language that compiles down to JS that provides the innovation. CoffeeScript.

If you want to take that a step further, look at ClojureScript. Want delimited continuations? Fine. All w/o requiring you to fork Node.js or CoffeeScript.

This is great, but I'd rather not use a fork of Node, and Joyent isn't going to integrate this, unfortunately. I wish that Google would just add yield to V8, but they won't do that unless Apple adds it to whatever the hell their interpreter is called now (Squirrelfish?).
I really like this idea, but it doesn't seem to do error handling correctly.

This is not going to catch most errors occurring in an asynchronous APIs:

    try {
      asyncOperation(function(err, result) {
         // ...
      });
    } catch (e) {
      // ...
    }
Most errors will occur asynchronously, thus the convention of "err" being the first argument to asynchronous API callbacks in Node.

Unless this supports that convention, your code should actually look like:

    async function magic() {
      try {
        // code here
        await err, bar = doSomething();
        if (err) {
          throw err;
        }
        // more code here
        await err, boo = doAnotherThing();
        if (err) {
          throw err;
        }
        // do even more stuff here
      }
      catch (e) {
        // handle the error
      }
    }
...or something similar. It's better than the alternative, but not great.

This certainly could support the "err" first convention, but APIs that don't use that convention wouldn't work correctly.

Not necessarily. A callback function with the standard signature under the hood is implied. If the author is controlling flow into and out of these execution contexts there is no reason an error passed to the implicit callback cannot cause an exception to be thrown into the original execution. Throwing exceptions into coroutines is a fairly normal pattern.
True, but the existing Node APIs don't do that, they return the error as an argument to the callback.
The wiki page says:

    // these two lines are equivalent
    await a, b, c, d = moo(1, 2, 3);
    moo(1, 2, 3, function(a, b, c, d) {} );
So I don't see why not...

    // ...these two lines would be also equivalent
    await error, foo = bar(1, 2, 3);
    bar(1, 2, 3, function(error, foo) {} );
Exactly. The error object is a return value of the "await"-ed function, not an exception that's thrown.

Here's a concrete example. Normally in Node you do something like this:

  fs.readFile('/etc/passwd', function (err, data) {
    if (err) {
      // handle error
    }
    // normal processing
  });
The await version would look like this:

  await err, data = fs.readFile('/etc/passwd');
  if (err) {
    // handle error
  }
  // normal processing
Ideally it would look like this:

  try {
    await data = fs.readFile('/etc/passwd');
    // normal processing
  } catch (error) {
    // handle error
  }
This is certainly something that is doable and crossed my mind, but I did not want to have the await be restricted to functions that conform to the typical return arguments. For now at least.
How about

  try await foo = bar(baz)
as shorthand for

  await error, foo = bar(baz)
  if (error) throw error
I wrote a library (30 loc) to handle sequence and parallel flows.

Using only that, I rarely go over 80 character column limit that I impose on myself. There is absolutely zero callback spaghetti whether it's 2 or 25 functions deep in the chain.

Tbh, callback spaghetti only happens to newer async programmers in the same way that a newer programmer will write arrow code with if/else statements.

It's simply not a problem that needs to be addressed other than educating people who are new to node.js with some example tutorials that use an async helper library.

got a link? I'd be interested to know how it deals with errors / exceptions, for example.
(comment deleted)
Looks concise, but doesn't much help with the fundamental problems of callback code - exceptions don't work right, and "callbacks all the way down" whenever something starts having any async computation.
what do you mean by this -> "callbacks all the way down" whenever something starts having any async computation.
Imagine function a calls b() and function b calls c(), and they're all sync.

Now, let's say c() changes and needs to call someAsyncFunction() and provide a callback. Which means c itself needs to take a callback. Which means b needs to provide a callback, so b needs to also take a callback, so a needs to provide a callback. And so forth.

Callbacks are infectious - once anybody in the call stack needs one, everybody needs one even if they just pass it on down the stack. Unless you don't need to do anything with return values, but that's fairly rare.

I've been using: https://github.com/caolan/async

It looks pretty decent in CoffeeScript:

  blah = (done) ->
    async.series [
      (next) -> foo next
      (next) => @bar.baz 1, 2, 3, next
      (next) ->
        x = y
        z w next
    ], done
So instead of spaghetti code (a tangled mess) it now is macaroni code (small pieces of code everywhere, with unclear execution flow)? :-)

At least that's my experience with structuring asynchronous event-driven programs (without coroutines).

pretty much but I find it quite readable and maintainable. The execution flow is very clear though if you look at the comments in my gist.
Yes :) Still waiting for a proper execSync ( https://github.com/joyent/node/issues/1167 ).

Without it small build/utility scripts turn into macaroni cheese unless you resort back to Python/Ruby/Bash - more languages, harder to maintain.

I think this is coroutines under the hood?
i agree with you, i don't see it as a big issue. anyway: i'm excited that there are new/other paradigms out there. i'm not yet convinced that this the ultimate (if there is such a thing) solution, but i will give it a try.
IMHO the fact that the accepted solution to this problem is "use an async helper library" is a bad sign. This is so fundamental it should be part of the language, or at least runtime.
Actually that's a good point. I'd like to change my stance to it would be nice to have this language feature, but there are current workarounds that are decent.
Addressing the same problem (and more) in both node and the browser, via compiling-JS-to-JS: http://onilabs.com/stratifiedjs

(disclaimer: I am the guy who worked on the now-defunct `defer` support in Coffeescript, and am now working with the onilabs folk on the stratifiedJS runtime)

I don't accept that there's a "problem" that needs "fixing".
There's room for improvement. Most programmers shouldn't need to know how to write asynchronous code just as they shouldn't need to know how to write concurrent code. Consider mutexes vs Java's 'sychronized' vs transactional memory: there's a way forward, and it's to reduce complexity for the average programmer.

Remember that not everyone is as awesomely brilliant as you are.

Reduce complexity: sure. But if we have requirements that can be well defined within an async FP paradigm, then maybe an async FP environment isn't such a 'problem'... and many of us "average programmers" already know lisp. If nodejs doesn't suit everyone, probably not everyone should use nodejs. But if we're going to have a go at it, let's not immediately dismiss node's natural style as "an absolute mess"
Handling concurrency should be an accepted part of the skill set of GUI development.

Writing a user interface that can respond to user input, whilst also being able to handle and respond to a long running data access or computational requests is a concurrent problem.

Having a single threaded model with callbacks, like the JavaScript browser model is one of the less complex ways to handle this.

I agree that a less complex model is appropriate for some developers/applications. But to call yourself a "skilled-professional GUI-developer", you need to get awesomely brilliant enough to handle this.

You don't have to use inline functions all the time, you can do it like this:

  function foo () {.... }
  function bar () {....foo(); }
  function barfoo () {....bar(); }

  doSomething(barfoo);
And remember that 'bind' can save your life

this.io.sockets.on('connection', this.onConnection.bind(this));

That "callback spaghetti" is called continuation-passing style.

"Programs can be automatically transformed from direct style to CPS." [1]

Do the math.

[1] http://en.wikipedia.org/wiki/Continuation-passing_style

I don't fully understand why this makes callback hell better just because it is known under a nicer name. CPS has it's use cases, but application code is not one of them.
Specifically, CPS was intended as a mechanical program transformation to remove the stack, not a style for programmers to actually write their programs in.
And in a lot of other languages writing in CPS incurs a performance penalty (lucky for me it seems to be < 1 order of magnitude in clojure); I assume this is the case in node.js too?
1) use async.js (classic javascript) 2) learn asynchronous patterns 3) profit