Item 61 shows how asynchronous APIs perform potentially expensive I/O operations without blocking the application from continuing doing work and processing other input. Understanding the order of operations of asynchronous programs can be a little confusing at first. For example, this program prints out "starting" before it prints "finished", even though the two actions appear in the opposite order in the program source:
downloadAsync("file.txt", function(file) { console.log("finished"); }); console.log("starting");
The downloadAsync call returns immediately, without waiting for the file to finish downloading. Meanwhile, JavaScript’s run-to-completion guarantee ensures that the next line executes before any other event handlers are executed. This means that "starting" is sure to print before "finished".
The easiest way to understand this sequence of operations is to think of an asynchronous API as initiating rather than performing an operation. The code above first initiates the download of a file and then immediately prints out "starting". When the download completes, in some separate turn of the event loop, the registered event handler prints "finished".
So, if placing several statements in a row only works if you need
to do something after initiating an operation how do you sequence
completed asynchronous operations? For example, what if we need
to look up a URL in an asynchronous database and then download
the contents of that URL? It’s impossible to initiate both requests
back-to-back:
db.lookupAsync("url", function(url) { // ? }); downloadAsync(url, function(text) { // error: url is not bound console.log("contents of " + url + ": " + text); });
This can’t possibly work, because the URL resulting from the data-
base lookup is needed as the argument to downloadAsync, but it’s not
in scope. And with good reason: All we’ve done at that step is initiate
the database lookup; the result of the lookup simply isn’t available
yet.
The most straightforward answer is to use nesting. Thanks to the power of closures (see Item 11), we can embed the second action in the callback to the first:
db.lookupAsync("url", function(url) { downloadAsync(url, function(text) { console.log("contents of " + url + ": " + text); }); });
There are still two callbacks, but the second is contained within the first, creating a closure that has access to the outer callback’s variables. Notice how the second callback refers to url.
Nesting asynchronous operations is easy, but it quickly gets unwieldy when scaling up to longer sequences:
db.lookupAsync("url", function(url) { downloadAsync(url, function(file) { downloadAsync("a.txt", function(a) { downloadAsync("b.txt", function(b) { downloadAsync("c.txt", function(c) { // ... }); }); }); }); });
One way to mitigate excessive nesting is to lift nested callbacks back
out as named functions and pass them any additional data they need
as extra arguments. The two-step example above could be rewritten as:
db.lookupAsync("url", downloadURL); function downloadURL(url) { downloadAsync(url, function(text) { // still nested showContents(url, text); }); } function showContents(url, text) { console.log("contents of " + url + ": " + text); }
This still uses a nested callback inside downloadURL in order to combine the outer url variable with the inner text variable as arguments to showContents. We can eliminate this last nested callback with bind (see Item 25):
db.lookupAsync("url", downloadURL); function downloadURL(url) { downloadAsync(url, showContents.bind(null, url)); } function showContents(url, text) { console.log("contents of " + url + ": " + text); }
This approach leads to more sequential-looking code, but at the cost of having to name each intermediate step of the sequence and copy bindings from step to step. This can get awkward in cases like the longer example above:
db.lookupAsync("url", downloadURLAndFiles); function downloadURLAndFiles(url) { downloadAsync(url, downloadABC.bind(null, url)); } // awkward name function downloadABC(url, file) { downloadAsync("a.txt", // duplicated bindings downloadFiles23.bind(null, url, file)); } // awkward name function downloadBC(url, file, a) { downloadAsync("b.txt", // more duplicated bindings downloadFile3.bind(null, url, file, a)); } // awkward name function downloadC(url, file, a, b) { downloadAsync("c.txt", // still more duplicated bindings finish.bind(null, url, file, a, b)); } function finish(url, file, a, b, c) { // ... }
Sometimes a combination of the two approaches strikes a better balance, albeit still with some nesting:
db.lookupAsync("url", function(url) { downloadURLAndFiles(url); }); function downloadURLAndFiles(url) { downloadAsync(url, downloadFiles.bind(null, url)); } function downloadFiles(url, file) { downloadAsync("a.txt", function(a) { downloadAsync("b.txt", function(b) { downloadAsync("c.txt", function(c) { // ... }); }); }); }
Even better, this last step can be improved with an additional abstrac-
tion for downloading multiple files and storing them in an array:
function downloadFiles(url, file) { downloadAllAsync(["a.txt", "b.txt", "c.txt"], function(all) { var a = all[0], b = all[1], c = all[2]; // ... }); }
Using downloadAllAsync also allows us to download multiple files
concurrently. Sequencing means that each operation cannot even
be initiated until the previous one completes. And some operations
are inherently sequential, like downloading the URL we fetched from
a database lookup. But if we have a list of filenames to download,
chances are there’s no reason to wait for each file to finish download-
ing before requesting the next. Item 66 explains how to implement
concurrent abstractions such as downloadAllAsync.
Beyond nesting and naming callbacks, it’s possible to build higherlevel abstractions to make asynchronous control flow simpler and more concise. Item 68 describes one particularly popular approach. Beyond that, it’s worth exploring asynchrony libraries or experimenting with abstractions of your own.
Things to Remember
? Use nested or named callbacks to perform several asynchronous operations in sequence.
? Try to strike a balance between excessive nesting of callbacks and awkward naming of non-nested callbacks.
? Avoid sequencing operations that can be performed concurrently.