| layout | post |
|---|---|
| title | Asynchronous patterns in Node.js |
| author | NodeKC |
| tags |
First, let's see the problem we're trying to avoid:
{% highlight javascript %} for (var i = 0; i < 100; i++) { setTimeout(function () { console.log(i); }, 100); } console.log('You might think this gets printed last.') {% endhighlight %}
Copy the code above into a new file that you will use for the rest of this lab. The points being illustrated require to be running from a .js file.
Notice the only value printed is 100, which is the terminal condition for this loop. This is because the callback for setTimeout doesn't actually get executed until the for loop has completed. Any other statements after the for loop will also be executed before any of the callbacks. This is, in part, because Node is single threaded. The event loop hasn't had a chance to invoke your callback. In any case, all of the callbacks are being fired with the same value of i.
How do we avoid this? We introduce a closure. In JavaScript a closures are achieved through the use of functions. By creating a closure to capture the value of i we're able to ensure we print the correct value for each iteration of the loop. When performing asynchronous operations in a loop be sure to capture variables you intend to use.
{% highlight javascript %} for (var i = 0; i < 100; i++) { (function (i) { setTimeout(function () { console.log(i); }, 100); })(i); // The current value of i is captured by self executing function } console.log('this still gets printed first.'); {% endhighlight %}
JavaScript, unlike many other languages, only has a function level scope. This means that variables declared within a for loop or if if block are accessible outside of the brackets.
A better way to do this would be to avoid creating a new function with each iteration of the loop like so:
{% highlight javascript %} var printNumberLater = function (i) { setTimeout(function () { console.log(i); }, 100); };
for (var i = 0; i < 100; i++) { printNumberLater(i); }
console.log('this still gets printed first.') {% endhighlight %}
So how do we go about printing out a something after the loops work has been done?
More often than not there's a need to invoke a set of tasks asynchronously and then start a new set of tasks upon completion:
{% highlight javascript %} var totalNumbersToPrint = 100; var finishedCount = 0;
var printNumberLater = function (i) { setTimeout(function () { console.log(i);
finishedCount++;
if (finishedCount === totalNumbersToPrint) {
console.log('this finally gets printed after all work is done!');
}
}, 100); };
for (var i = 0; i < totalNumbersToPrint; i++) { printNumberLater(i); } {% endhighlight %}
In the example above, note that we keep track of how many times the setTimeout callback is invoked. Once the value matches the number of callbacks we're waiting on we can continue with printing out our message. This can get quite ugly the more complex your application becomes. A very good library for dealing with this is async. Here's an example of how async could be used to clean up the code above.
Start from a brand new directory and install the async module.
{% highlight bash %} mkdir async-example cd async-example npm init /* use default settings */ npm install async --save {% endhighlight %}
{% highlight javascript %} var async = require('async'); var workToBeDone = [];
var printNumberLater = function (i) { return function (callback) { setTimeout(function () { console.log(i); callback(); }, 100); }; };
for (var i = 0; i < 100; i++) { workToBeDone.push(printNumberLater(i)); }
async.parallel(workToBeDone, function () { console.log('this still gets printed last'); }); {% endhighlight %}
Imagine the above where you had multiple things needing to happen in parallel. Remember how we kept around the finishedCount counter a few examples back? Keeping track of all the counters for more complicated examples could get extremely messy. Using async we're able to avoid these arbitrary counters and organize our code better. Another pattern to use is called promises.
Install the promise-extended npm package with the following command: npm install promise-extended. Then create a new file called promises.js with the following contents:
{% highlight javascript %} var p = require("promise-extended"); var Promise = p.Promise;
var operations = [];
var printNumberLater = function (i) { var prom = new Promise();
setTimeout(function () { console.log(i); prom.callback(); }, 100);
return prom.promise(); //Return the newly created promise right away. };
for (var i = 0; i < 100; i++) { operations.push(printNumberLater(i)); }
p.when(operations).then(function (bios) { console.log('executed after all operations complete.'); }); {% endhighlight %}
Promises are an abstraction that sit on top of asynchronous actions giving a concrete value to work with that represents a future value.
{% highlight javascript %} var Promise = require("promise-extended").Promise;
function asyncAction(){ var p = new Promise(); process.nextTick(function(){ p.callback(1); }); return p.promise(); }
asyncAction().then(function(res){ console.log(res); //1; });
{% endhighlight %}
In the above example we have the function asyncAction which returns a promises that will resolve on the next event loop with 1.
So where promises become a powerful in helping managing the callback madness is when managing multiple async actions in parallel or ensuring actions happen in a given order.
Example of managing execution order
{% highlight javascript %}
var Promise = require("promise-extended").Promise;
var asyncCount = (function(){ var currentCount = 0; return function(){ var p = new Promise(); process.nextTick(function(){ p.callback(++currentCount); }); return p.promise(); } }());
asyncCount() .then(function(currCount){ console.log(currCount); //1
//we return a promise from in here which will cause then to wait for this one to resolve.
return asyncCount();
})
.then(function(currCount){
console.log(currCount); //2
//returning another promise!
return asyncCount();
})
.then(function(count){
//now we have the result from the last asyncCount call which is 3
console.log(count); //3
});
{% endhighlight %}
Example of managing multiple async actions at once. Make sure to install the promise-extended node module with the following command:
{% highlight javascript %} npm install promise-extended --save {% endhighlight %}
{% highlight javascript %} var p = require("promise-extended"), Promise = p.Promise;
var asyncWait = function(timeout){ var ret = new Promise(); setTimeout(function(){ ret.callback(timeout); }, timeout); return ret.promise(); }
//use the when method to wait for all of the passed in promises to resolve. p.when( asyncWait(1000), asyncWait(900), asyncWait(800), asyncWait(700), asyncWait(600), asyncWait(500) ).then(function(res){ console.log(res); //[1000,900,800,700,600,500]; });
{% endhighlight %}
In the above example when returns a promise that waits for all the passed in promises to resolve.
###Error handling.
Error handling with promises is simplified greatly because you do not have to worry about errors unless your code cares about it directly.
{% highlight javascript %} var asyncCount = (function(){ var currentCount = 0; return function(){ var p = new Promise(); process.nextTick(function(){ p.callback(++currentCount); }); return p.promise(); } }());
asyncCount() .then(function(count){ console.log(count); //1 return asyncCount(); }) .then(function(){ throw new Error("Oops an error occurred"); }) .then(function(){ return asyncCount(); }) .then(function(res){/wont be called/}, function (err){ console.log(err.message); //Oops an error occurred ... }) {% endhighlight %}