Node.js 101: Flow Control

"I eat, sleep, and breathe PHP (or C++). How is flow control different in node.js?

The if/then/else constructs that you're used to aren't any different in node.js, but function calls are much much different. Take this code, for example:

var results = db.query(query);
console.log(results);

This won't do what you'd expect. The results variable will very likely be null. This is because nearly every function call in node.js that involves I/O, whether it's to the network or the filesystem takes a callback function as an argument, a function which will be executed only upon completion of the request. This is due to node.js's asynchronous nature.

Taking the above example, you'd write something like this, instead:

db.query(query, function(error, results) {
   if (!error) {
      console.log(results);
   }
});
console.log("Sent our query!");

The function that we passed in to db.query() will be executed when the query completes, and not a millisecond before. Also of note is that the text "Sent our query!" will, in most cases display before the results of the query do. If you're familiar with threading, you can think of this behavior as similar to threading, but not quite the same. (you don't need to issue locks, for example) But remember, the order in which these events happen is not guaranteed.

The problem with the above code, however, is that it doesn't scale well. Example:

db.query(query, function(error, users) {
   if (!error) {
      db.query(query, function(error, posts) {
         if (!error) {
            db.query(query, function(error, comments) {
               if (!error) {
                  console.log(comments);
               } else {
                  // Handle error
               }
            } else {
               // Handle error
            }
         }
      } else {
         // Handle error
      }
   } else {
      // Handle error
   }
});

Wow, that's ugly. Worse still, it's difficult to tell at a glance what error handling code handles what query. Some people like to call it "boomerang code". I personally call it "a migraine waiting to happen".

"Is there a better way to do this that won't put me into a deep dark depression?"

Yes. Enter node-seq. Seq is a way to chain functions in a way which prevents this boomerang effect.

Here's the above example with the benefit of Seq:

var seq = require("seq");
seq().seq(function() {
   db.query(query, this);

}).seq(function(users) {
   db.query(query, this);

}).seq(function(posts) {
   db.query(query, this);

}).seq(function(comments) {
   console.log(comments);
   // Here, we'd probably call our callback that was passed into this function, or just exit the program.

}).catch(function(error) {
   console.log(error);

});

Looks a lot nicer, doesn't it? In each function "block", the variable "this" is a callback function which takes us to the next function. It follows best practices for catching errors (as described in my post on error handling in Node.js), where the first argument is an error, and the second and subsequent arguments are data to pass on to the next function.

See that "catch" function at the end? If any of the callbacks from Seq was passed an error, processing is aborted and the catch() function is immediately called with the error.

"So how do I get a stack trace out of this?"

Whatever you do, don't throw an exception. Exceptions are evil! They tend to bring the entire node.js process to a screeching halt if uncaught. That's not desirable, especially if you're running a process with many inbound or outbound connections. There's an easier way to do this. Returning to my example from the previous blog post:


function talk(cb) {
   cb(null, "talk(): Eyes of Ganon are everywhere.");
}
 
function village(cb) {
   seq().seq(function() {
      talk(this);
   }).seq(function() {
      cb(); // Success!
   }).catch(function(error) {
      cb("village(): " + error);
   });
}
 
function world(cb) {
   seq().seq(function() {
      village(this);
   }).seq(function() {
      cb(); // Success!
   }).catch(function(error) {
      cb("world(): " + error);
   });
}

seq().seq(function() {
   world(this);

}).seq(function() {
    // Success!
   process.exit(0);

}).catch(function(error) {
   cb("ERROR: " + error);
   process.exit(1);

});

This will give you what amounts to a stack trace:

ERROR: world(): village(): talk(): Eyes of Ganon are everywhere.

"What about lists of items?"

Want to handle an array of items in parallel? Try this:

var posts = [];

seq(users).parEach(function(user) {
   getPosts(user, this);
}).seq(function() {
   console.log(posts);
});

This executes the getPosts() function on all of the users in parallel, but won't move to the function to print out the posts until all users have been processed. Also note that I defined a variable called posts in the parent context. The reason behind this is so that the getPosts() function (assuming it is in the same module) can append their results all to the same array. There are better ways to do this in Seq, and the documentation goes into more detail. I encourage you to check it out.

"Wait, I don't want to hammer the database with many concurrent queries at once..."

No problem. Substitute this line:

seq(users).parEach(function(user) {

..with this one:

seq(users).seqEach(function(user) {

Each function must finish by calling this() before it can be called again on the next element in the array.

Here's how you install Seq:

npm install seq

Have fun, and don't hesitate to contact me or leave a comment below if you have any questions or find these examples unclear.

3.088235
Average: 3.1 (68 votes)
Your rating: None