Pages

[NodeJS] How to behave synchronously in an asynchronous world

Dispite the power of asynchronous programming in Javascript, there's time when you want to execute code synchronously. An example is when you have an array of tasks that must be completed sequentially. In this post, we are going to look at different ways to implement this in NodeJS.

Settings

Image you want to simulate the activities in a restaurants. The tasks are: server takes order, chef cooks, server serves food, customers enjoy the food. Since the level of details of simulation can be expanded in the future, we'll maintain an array of all the tasks and pass it to a task runner to handle the tasks.


function order() { console.log('Server gets order.'); }
function cook() {
  fs.readFile('cookbook.txt', function() {
    console.log('Chef cooks food.');
  };
}
function serve() { console.log('Server serves food.'); }
function dine() { console.log('Customers enjoy food.'); }

function run(tasks) {
  // run tasks
}

var tasks = [order, cook, serve, dine];
run(tasks);

As you can see, among these tasks, cook is the longest one because chef must find the recipe in a cookbok before starting to cook. Obviously, it's not a very good restaurant as you can tell but you get the idea.

Naive attempt

So, we just need to iterate through the array of taks and call the corresponding function, right? However, if you just run a for loop like this.


function run(tasks) {
  _.each(tasks, function(task) {
    task();
  });
}

Then, you get back the result like this.


$ node naive.js
Server gets order.
Server serves food.
Customers enjoy food.
Chef cooks food.

The reason is, although, we iterate through the tasks in order, the functions are executed asynchronously and there is no guarantee that the first task will be completed before second task begins. Let's try to fix that.

DIY

First of all, we need a way to tell when a task is done. With Javascript, we can use callback function to do so. Let's change our function as following


function order(done) {
  console.log('Server gets order.');
  done();
}

function cook(done) {
  fs.readFile('cookbook.txt', function() {
    console.log('Chef cooks food.');
    done();
  };
}

function serve(done) {
  console.log('Server serves food.');
  done();
}

function dine(done) {
  console.log('Customers enjoy food.');
  done();
}

Now, we can recursively iterate through the array of tasks to execute each task in order. When a task is done, we use the callback function to move on with the next task.


function run(tasks) {
  function iterate(tasks, idx) {
    if (idx === tasks.length) return;
    tasks[idx](function() {
      iterate(tasks, idx + 1);
    });
  }

  iterate(tasks, 0);
}

With that we get our expected result.


$ node iterate.js
Server gets order.
Chef cooks food.
Server serves food.
Customers enjoy food.

Of course, as you may guess, this is not a new problem. There are different ways to solve this problem. There is a very popular library, async, that provides functions for working with asynchronous Javascript.

Async

For our problem, we can eith use series or eachSeries function


function run(tasks) {
  async.eachSeries(tasks, function(task) {
    task();
  });
}

In fact, if you look at the source code, their implementation is not much different from what we did above.

Another way to enforce synchronous behavior in our problem is to use promises.

Promises

A promise, according to the promise specification, represents the eventual result of an asynchronous operation. With a promise, we can do:

 

promise.then(function() { 
  // do something after promise has been fulfilled 
});

There are different libraries that implement promise. The one that I'm familiar with is q by Kris Kowal.

First, we need to convert our task function into functions that will return promises. Q gives us denodeify function for that. Once, it's done, we can keep chaining all the promises together.


var q = require('q'),
    deferred = q.defer(),
    promise = deferred.promise;

function run(tasks) {
  deferred.resolve();

  tasks = _.map(tasks, function(task) {
    return q.denodeify(task);
  });

  _.each(tasks, function(task) {
    promise = promise.then(function() {
      return task();
    });
  });
}

With ES6, we can also use generator to solve this problem. I will update this post or have another post to address this approach.

1 comment: