Coming from Python, Node.js quickly blew my mind with the whole concept of asychronous stuff. I don't think I actually understood managing things asynchronously for a few months after I started writing Javascript (lets be honest, I probably still don't completely understand it). While the wound is still fresh, I figured I would take some time to write a basic guide for navigating callbacks and promises in Node.js.
Why
If you've come from something like Python or other popular languages threading has likely been something that was either done automatically by a module or something you've had complete control over. In Node threading doesn't really exist, you have one thread so good luck and have fun!
With >1 threads you can do several things (perhaps things that are even blocking) at the same time without impacting the application as a whole (that is, other requests can still be served). With a single thread, a blocking action will block the application as a whole - if you make a synchronous call other requests will need to wait for that task to finish. This is clearly undesirable for things like web applications. What good is an application that can only serve one request at a time? Node addresses this with callbacks, and more recently promises.
Callbacks
A callback is a function that is executed after a task is completed. It usually looks something like this:
asynchronousFunction(argument, function(response) {
// When the function is finished doing whatever it is doing, this stuff is executed
// The callback takes any return values as arguments, so if asynchronousFunction returned 1+1, response would be 2
console.log("asynchronousFunction is done with the response", response);
});
You can also use a callback that is defined elsewhere like this:
asynchronousFunction(argument, callbackFunction);
function callbackFunction(response) {
// This is executed when the function is done doing its stuff
console.log("asynchronousFunction is done!");
}
In general, callbacks will first take an error argument and then other arguments that are returned by the function that is utilizing the callback. You can test to see if the error
object exists after each call to see if the call was successful:
asynchronousFunction(argument, function(error, response) {
if (error) {
console.log("Something broke!");
return;
}
// If this is executed there was no error!
});
The problem with callbacks is they are a little messy. Consider a chain of asynchronous functions that need to run one after another:
aFunction(argument1, function(response) {
// This runs after aFunction is done
bFunction(argument2, function(response2) {
// This runs after bFunction is done
cFunction(argument3, function(response3) {
// This runs after cFunction is done
});
});
});
Not too pretty right? It gets even uglier if you want to deal with errors properly at every step of the chain:
aFunction(argument1, function(error, response) {
// This runs after aFunction is done
if (error) { return error; }
bFunction(argument2, function(error, response2) {
// This runs after bFunction is done
if (error) { return error; }
cFunction(argument3, function(error, response3) {
if (error) { return error; }
// This runs after cFunction is done
});
});
});
Fortunately promises help make this much neater, easier to read and easier to control.
Promises
A promise is essentially an abstraction of a callback that uses simple to understand syntax and structures. A promise is just that, a promise to give something in the future. For example, say asynchronousFunction
from above makes a call to an API requesting all of the virtual machines on an account. That process isn't instant, so it returns a promise which is a placeholder for the list of all the virtual machines on an account. A promise can be either resolved
or rejected
. A resolved promise is one that fulfilled its promise so to speak and actually has the data you were promised. A rejected promise happens if an error is encountered.
So to rewrite the asynchronousFunction
from above using promises you would do something like this:
asynchronousFunction(argument).then(function(response) {
// Executed after function is done
});
The flow works just like it states, do this function, then
do this. You can also pass around promises pretty easily:
function aFunction() {
return asynchronousFunction(argument);
}
aFunction.then(function(response) {
// Executed after asynchronousFunction is done
});
Error management is a breeze:
asynchronousFunction(argument).then(function(response) {
// This is executed if the promise is resolved
}).catch(function(error) {
// This is executed if the promise is rejected
});
Promises really shine when you have to link a bunch of stuff together. Recall how callbacks looked with multiple functions and then take a look at the syntax used by promises:
aFunction(argument).then(function(response) {
// Executed after aFunction is done
return bFunction(argument2);
}).then(function(response2) {
// Executed after bFunction is done
return cFunction(argument3);
}).then(function(response3) {
// Executed after cFunction is done
return "All done";
});
Much cleaner and easier to read in my opinion. Errors can also propagate up the promise chain fairly easily as long as all promises in the chain are returned properly. A single catch statement for the entire chain will catch any errors thrown while processing those promises:
aFunction(argument).then(function(response) {
// Executed after aFunction is done
return bFunction(argument2);
}).then(function(response2) {
throw new Error("foobar");
// Executed after bFunction is done
return cFunction(argument3);
}).then(function(response3) {
// Executed after cFunction is done
return "All done";
}).catch(function(error) {
// Caught error from bFunction and stops executing
});
Bluebird is (in my opinion) the best library to use for this sort of thing. You can do tons of really cool stuff with it, including converting things that require callbacks to promises using promisifyAll
:
var libraryThatUsesCallbacks = require("callbackLibrary");
var Promise = require("bluebird");
Promise.promisifyAll(libraryThatUsesCallbacks);
// libraryThatUsesCallbacks can now be used like so
libraryThatUsesCallbacks.aFunctionAsync(argument).then(function(response) {
// I'm a promise now!
});
// Bluebird adds new functions that basically map to the old function name + async, so bFunction would become bFunctionAsync
This can generally be done for all functions that use the callback(error, arguments)
pattern though Bluebird does support other arrangements as well. Bluebird supports a wide array of other things, I would recommend taking a look at the API documentation.
Conclusion
Given the single threaded nature of Node applications learning how to write asynchronous code, and more importantly, think asynchronously, is necessary to becoming a successful Node developer. This isn't an easy task, especially if you are not used to thinking about things in this way, it took me a long time to fully grasp the concepts and properly use asynchronous functions so don't worry if it takes a bit. Hopefully this post will help you learn the basics if you are looking to develop Node applications. Please feel free to leave any questions in the comments.