Javascript is a highly asynchronous programming language. No matter if you play with ajax calls or a MongoDb query in Node.js, you will step over loads of callbacks being executed asynchronously.
When working with synchronous programming, you know that your code will execute sequentially, starting from the top of the code going downwards.
But When code is executed asynchronously, you provide a callback function to a the asynchronous function. The asynchronous function will then go on and be executed in the background, at the same time as your main code continues to execute. When the asynchronous function is done, the callback function will be called, and you have code being executed in parallel.
$.get('/', function(res){ console.log('page was loaded'); }); console.log('hello world'); // Hello world will be written first // Then 'page was loaded'
Here we use the jQuery function get to get the root html page from our server. We provide an anonymous function as a parameter to $.get. It will then be called by $.get when the web page has been fetched. Hence the name “callback function”. Since get is executed asynchronously, the console.log on the row after the $.get call, “Hello world” will be outputted before “page was loaded”.
This is really nice, since it allows us to do stuff while the page is loading, or while we do something else that takes some time.
Great ha?
But, and this is a rather big one, asynchronous code quickly becomes horribly spaghetti-codish. You have callbacks for successful get’s, callbacks on errors, and when you put an asynchronous call within another, the code will look something written in the eighties.
var sleep = function(name, callback){ setTimeout(function(){ console.log(name); callback(); }, 1000); } sleep('Hello world', function(){ sleep('How are you?', function(){ sleep('Nesting is to be avoided', function(){ console.log('Finally done'); }); }) });
That’s not nice, right? Nesting is awful, hard to read and not fun to debug. But we need our asynchronous functions, since we don’t want our webpage to stop while the ajax is loading. So lets do something about the nesting.
To handle the nesting, we have deferred objects that returns promises . A promise is an object with functions for adding callbacks and being able to check an asynchronous function’s status. Sounds strange, but it is pretty nice:
var sleep = function(name){ var dfd = new $.Deferred(); // This is the object which stores callbacks and // handles status changes. setTimeout(function(){ console.log(name); // Instead of calling a callback function as before, we simply notify // our Deferred object that we are done: dfd.resolve(name + ' is done'); }, 1000); // We return our promise object to which a sequence of callback functions // can be attached without having to nest. return dfd.promise(); } sleep('Hello world') // The object we add the "then" call to is the promise. .then(function(){ return sleep('How are you?'); }) // And after that another call .then(function(){ return sleep('Nesting is to be avoided'); }) // And done... .then(function(){ console.log('Finally done'); });
The function “then” is run as soon as the deferred object’s resolve function has been called, so instead of having to nest the functions we have a nice little sequence, and the code looks much better.
Trying to make this even a bit more clear, let’s store our anonymous functions in named variables:
function getNamedPromiseFunction(name){ return function(message){ console.log(message); var dfd = new $.Deferred(); setTimeout(function(){ dfd.resolve(name + " done"); },1000); console.log('running ' + name); return dfd.promise(); }; } function done(message){ console.log(message); console.log('done'); } var a = getNamedPromiseFunction('a'); var b = getNamedPromiseFunction('b'); var c = getNamedPromiseFunction('c'); a('Starting').then(b).then(c).then(done); // Starting // running a // a done // running b // b done // running c // c done // Done
Easy, ha? Notice also that “then” will use the parameter from the resolve call and pass it into the next function.
Another great feature with promises is that it is simple to handle errors. It’s enough to provide one fail callback function for the whole chain of then calls:
function getNamedPromiseFunction(name){ return function(message){ console.log(message); var dfd = new $.Deferred(); setTimeout(function(){ // This time we reject the Deferred function. dfd.reject(name + " no way"); },1000); console.log('running ' + name); return dfd.promise(); }; } function done(message){ console.log(message); console.log('done'); } function error(message){ console.log(message); console.log('Something went wrong'); } var a = getNamedPromiseFunction('a'); var b = getNamedPromiseFunction('b'); var c = getNamedPromiseFunction('c'); // We add a fail callback for handling errors. a('Starting').then(b).then(c).then(done).fail(error); // Starting // running a // a no way // Something went wrong
The fail will be called if any of the deferred objects are rejected. Compare that to putting in error handling in a nested asynchronous code.
Of course, it would be really nice if we could write something like this:
pipe(a, b, c, done).fail(error);
Lets create that pipe function just for fun. (I use underscore.js for storing the arguments in an array)
function getNamedPromiseFunction(name){ return function(message){ if(message) console.log(message); // create a deferred object var dfd = new $.Deferred(); // Make it asynchronous with setTimeout setTimeout(function(){ // resolve the deferred object dfd.resolve(name + " done"); },1000); console.log('running ' + name); // return a promise return dfd.promise(); }; } function done(message){ var dfd = new $.Deferred(); dfd.resolve(); console.log(message); console.log('done'); return dfd.promise(); } function error(message){ console.log(message); console.log('Something went wrong'); } var a = getNamedPromiseFunction('a'); var b = getNamedPromiseFunction('b'); var c = getNamedPromiseFunction('c'); var pipe = function(){ // arguments is an object, lets transform that to an array var arr = _.toArray(arguments); // We create a deferred object for the pipe function, so that // it can be a part of a promise chain var dfd = new $.Deferred(); var cur = arr[0]; // Our fail callback is simple - it simply reject our deferred object, // passing the message parameter on. var failFunc = function(message){ dfd.reject(message); }; // a function for finding the next function to run in our piped array, // calling it with the result of the previous functions resolve parameter // as parameter. var runNext = function(res){ var i = arr.indexOf(cur); if(i+1 < arr.length){ // we assume all functions return promises. cur = arr[i+1]; cur(res).then(runNext).fail(failFunc); }else{ // if there are no more functions to run, let's resolve this // pipe. dfd.resolve(res); } }; // Start running the parameters. runNext is an asynchronous recursive // function.cur().then(runNext).fail(failFunc); // return a promise so we can use pipe in a chain. return dfd.promise(); } console.log('Starting'); pipe(a,b,c, done).fail(error); // Starting // running a // a done // running b // b done // running c // c done // done
As you can see, deferred objects and promises are easy to work with and results in really good looking code.