Taming Asynchronous JavaScript with Async.js
JavaScript is often described as asynchronous, meaning that function calls and operations don’t block the main thread while they execute. This is true in some situations, but JavaScript runs in a single thread, and therefore has lots of synchronous components.
It’s more accurate to say that many function calls in JavaScript can be made asynchronously. Web programming essentials like DOM event handlers and AJAX calls are asynchronous, and because these common tasks make up a great bulk of JavaScript programming, JavaScript itself is often labeled “asynchronous.”
When a function runs asynchronously, that means it doesn’t stop subsequent function calls from running while it finishes. Take this example:
var users = [];
$.get(‘/users’, function(data) {
users = data;
});
renderUsersOnPage(users);
Since $.get()
is asynchronous, the next functional call (to render the users) will execute immediately, whether the fetch call has finished or not. This means that our data won’t be available to us when we try to render it, because the earlier call to fetch them from the API is still being executed!
So, the main problem with asynchronous programming is how to call asynchronous functions that depend on each other. How do you ensure function A has finished before calling function B, if you can’t rely on function A blocking until it’s done?
The Callback Pattern
An early solution to this problem was the callback pattern.
In the callback pattern, you pass a function as an argument to an asynchronous method. When that call has finished, the host function executes your callback.
In fact, the first example above includes an example of a callback. The jQuery method $.get()
takes two arguments: the first is the URL you want to fetch, and the second is the callback function to run once the data arrives. In our example above, we’re passing in an anonymous function as the second argument to $.get()
. That function takes the server data as an argument, and only executes when the data is ready.
To adapt our earlier (erroneous) code to the callback pattern, we would just need to move renderUsersOnPage()
into the $.get()
callback:
var users = [];
$.get(‘/users’, function(data) {
users = data;
renderUsersOnPage(users);
});
This ensures renderUsersOnPage()
only runs after the users have been fetched.
Callback Hell
Callbacks themselves are not bad. But the prevalent misuse of callbacks in our industry has led to the notion of callback hell: code that nests callbacks so heavily as to be unreadable.
Just as you would avoid nesting lots of if/else statements like this:
if (condition) {
if (condition) {
if (condition) {
console.log('true of all the conditions')
}
}
}
You should avoid nesting lots of callbacks like this:
var users = [];
fetchUsers(function() {
renderUsersOnPage(function() {
fadeInUsers(function) {
loadUserPhotos(function() {
// you get the idea
});
});
});
});
There are several problems with the code above.
The first is that heavily-nested code is difficult to read later on. It’s hard to follow flow control and difficult to isolate problem spots. The second reason is that the code above tightly couples one function to another. This makes it hard to reuse in other situations, hard to incrementally improve over time, and hard to test.
So what else can we use?
Enter: Promises
One alternative to managing asycnhronous code is to use promise objects. There are lots of different implementations of the JavaScript promise object, but the core concepts are all the same.
A promise represents a value that is still being fetched or computed. A promise object can answer questions about its state, like “has my data arrived yet?” or “were there any errors during the fetch?” Once a promise object has its value fulfilled, it can alert other objects that its data has arrived and any dependent functions can be run.
Promises are very useful, but we will be focusing on the async.js library in this post. Async.js is easier to get started with, and is especially adept at refactoring callback soup. Promises are implemented differently across browsers, and the popular promise libraries (Q, jQuery deferreds) each deserve their own blog post.
You can start using async.js today without any knowledge of promises.
The Async.js approach
The async.js library includes two families of functions: those that help manage asynchronous control flow, and those that help with functional JavaScript programming. There isn’t a bright line, and many of async.js’s methods fit into both of these categories. But we’ll be looking specifically at its asynchronous-minded functions.
Let’s start with an example. Look back at the callback-soup code above, and then compare it with this async.js version:
var users = [];
async.series([
function(callback) {
fetchUsers(callback);
},
function(callback) {
renderUsersOnPage(callback);
},
function(callback) {
fadeInusers(callback);
}
function(callback) {
loadUserPhotos(callback);
}
]);
What’s happening here? Async.series()
sees an array of functions and executes each one in order, not starting on one until the previous has finished. The async
object automatically passes each function a callback argument. Once we manually call the callback, async
knows that it’s safe to move on to the next function.
Note that this example assumes that each of our named functions accepts a callback argument and only calls it once its individual work has finished. This is standard practice.
You may be wondering why we’re using callbacks if we just determined that callbacks are bad. In fact, we showed that callbacks can be misused if nested too deeply, but there is nothing inherently wrong with callbacks themselves. The example above shows how callbacks can be used effectively. The code never nests more than one callback deep, with the callback acting as simple message that a particular function has finished.
Since async.js automatically provides each function in the array with the callback as the first argument, we can simplify the code even further:
var users = [];
async.series([
fetchUsers,
renderUsersOnPage,
fadeInusers,
loadUserPhotos
]);
Each of these named functions will be called as they were before. We’re just removing the anonymous function middlemen from the previous examples.
Another handy async.js function is async.eachSeries()
. That applies an asynchronous function to each item in an array in series. For example, say you have a list of users, each of which needs to post its profile data to remote server log. Order matters in this case because the users in your array are sorted.
This regular code has no way of guaranteeing that your requests are posted in order:
users.forEach(function(user) {
user.postProfileToServer();
});
A whole host of network issues could delay some of those requests and expedite others, so your user array ordering is not respected. Async.eachSeries()
to the rescue:
async.eachSeries(users, function(user, callback) {
user.postProfileToServer(callback);
});
Again, async.js supplies us with a callback that we can use to advance it on to the next iteration. The primary difference between the last two examples is that the first assumes that the user.postProfileToServer()
calls will run in order simply because they were called in order. With regular JavaScript, that isn’t a sound assumption. Using async.eachSeries
takes into account that user.postProfileToServer()
is asynchronous and ensures that the iterations run in series.
Conclusion
The async.js library provides a clean, easy to use alternative to callback hell and the complexity of promises. All the async.js methods have similar APIs, centering around the passed-in callback function parameter to manage asynchronous code. I encourage you to browse async.js’s documentation for a complete list of methods. Though most everyone writes callback soup while they’re learning best practices, async.js offers clean solutions that help make your code more readable, testable, and refactorable.
P.S. Do you use callbacks or promises in your JavaScript code? Have you tried async.js? Which do you prefer, and why? Let us know by leaving a comment!
Share your thoughts with @engineyard on Twitter
OR
Talk about it on reddit