i'm steve

Callback, Promise, Generator - Nodejs

Sun 06 December 2015

Nodejs is a javascript runtime built upon Google's V8 engine which features event-driven & non-blocking IO model, making it a good option for writing scalable & efficient web applications.

Callback

If you have ever written Nodejs code, you will be so familiar with callbacks. It's a functional parameter passed to a method call, where it will be called only when the operation is done or failed. e.g.

var fs = require('fs');

// fs.readFile(<file>, <callback>);
fs.readFile('/tmp/file', function(err, data) {
  if (err) throw err; // throw err incase
  console.log(data); // do something with the returned data
});

There's no need to wait for the readFile from finishing (non-blocking), and when it does finish the file reading operation, it will trigger the function (callback) which was passed as the second parameter.

It works great and looks fine, until you started nesting a few of those method calls. Say you want to copy all the files inside a directory to another:

var fs = require('fs');
var path = require('path');

fs.readdir('/tmp', function(err, files) {
  files.forEach(function(file) {
    var abs_file = path.resolve('/tmp', file);
    fs.stat(abs_file, function(err, stats) {
      if (stats.isFile()) {
        fs.readFile(abs_file, function(err, data) {
          if (err) throw err;
          var new_file = path.resolve('/new-tmp',file);
          fs.writeFile(new_file, data, function(err) {
            if (err) throw err;
            console.log("copied " + file);
          });
        });
      }
    });
  });
});

It's messy and not intuitive to read, not to mention all those open/close brackets. That's the Callback Hell. This happens when writing code that depends on the result of the previous operations.

Promise

Promise is introduced in ES6 for deferred operations or async processes which acts like a proxy for result that's not ready at time but in the future.

A Promise can only be in one of the following three states: 1.Pending 2.Resolved 3.Rejected

// Syntax: new Promise(function(resolve, reject) {...}).

new Promise(function(resolve, reject) {
  doAsyncOperation(function(err, data) {
    if (err) return reject(err);
    resolve(data);
  });
});

One important feature of Promise is chaining, where you can connect a number of async operation (Promise) in a specific order using the then method, and once the current async operation is done, it will automatically set off the next.

// Assume there're three promise function that are pre-defined:
// promise1, promise2, promise3

promise1.then(function(result) {
  return promise2(result);
}).then(function(result) {
  return promise3(result);
}).then(function(result) {
  console.log(result); // final result after 3 async operations
}).catch(function(reason) {
  console.log(reason); // catching any error or reject inside the promise chain
});

As seen from the above snippet, it makes code much easier to read, and reduces the chance of falling into Callback Hell;

Generator

If you have written Python before, it's likely you have came across Generator. Generator is like a iterator/function that yields one value at a time instead of returning all at once. It's particularly useful when generating array values that's too big to fit into memory all at once; or when it's likely only part of the array values would be needed in which generating all upfront is not effective.

So what Generator has to do with Nodejs you may ask? When it's combined with generator-based-control-flow library, it enables you to write async code as if it's synchronous and elminiate the daunting callback pyramid. Co is one of those lib that I would recommend. Let's rewrite the above example using Co + Generator:

co(function*() {
  // the next *yield* will only run after the previous *yield* is done
  // like sync code
  var result1 = yield promise1();
  var result2 = yield promise2(result1);
  var result3 = yield promise3(result2);
  return result3;
}).then(function(result) {
  // single 'then' to handle the final return value
  console.log(result);
}, function(err) {
  console.log(err);
});

It's obvious the code is now far more readable and is less likely to introduce bugs compared with callbacks.

Tips

To start using or converting any existing project to use Co + Generator, reads the steps below:

  1. Can convert Callback-Methods using some promisify library such as Bluebird, it should be a simple import & automatic, as long as those code follows the Nodejs callback convention -- function(err, data) {...}
  2. Promises, Thunks, Generator are yieldable (Promise are preferred)
  3. Wrap all those yieldables (Pomises) inside a Co function.
  4. Enjoy!

Nodejs + Co + Promise = Lovely

The combo lets you write Nodejs code as if you are writing for synchronous application, while giving you free access to its non-blocking & network scalability benefit. It serves a really good foundation for building realtime web application quick & easy.

MORE POST