Managing Callback Hell
Problem
You want to do something such as check to see if a file is present, and if so open it and
read the contents. Node provides this functionality, but to use it asynchronously, you
end up with nested code (noted by indentations) in the code that makes the application
unreadable and difficult to maintain.
Solution
Use a module such as Async. For instance, in Example 11-2 we saw definitely an example
of nested callbacks, and this is a fairly simple piece of code: open a file, write two lines
to it, and then read them back and output them to the console:
var fs = require('fs'); fs.open('newfile.txt', 'a+',function(err,fd){ if (err) { console.log(err.message); } else { var buf = new Buffer("The first string\n"); fs.write(fd, buf, 0, buf.length, 0, function(err, written, buffer) { if (err) { console.log(err.message); } else { var buf2 = new Buffer("The second string\n"); fs.write(fd, buf2, 0, buf2.length, 0, function(err, written2, buffer) { if (err) { console.log(err.message); } else { var length = written + written2; var buf3 = new Buffer(length); fs.read(fd, buf3, 0, length, 0, function( err, bytes, buffer) { if(err) { console.log(err.message); } else { console.log(buf3.toString()); } }); } }); } }); } });
Notice the messy indentation for all the nested callbacks. We can clean it up using Async:
var fs = require('fs'); var async = require('async'); async.waterfall([ function openFile(callback) { fs.open('newfile.txt', 'a+',function (err, fd){ callback(err,fd); }); }, function writeBuffer(fd, callback) { var buf = new Buffer("The first string\n"); fs.write(fd, buf, 0, buf.length, 0, function(err, written, buffer) { callback(err, fd, written); }); }, function writeBuffer2(fd, written, callback) { var buf = new Buffer("The second string\n"); fs.write(fd, buf, 0, buf.length, 0, function(err, written2, buffer){ callback(err, fd, written, written2); }); }, function readFile(fd, written, written2, callback) { var length = written + written2; var buf3 = new Buffer(length); fs.read(fd, buf3, 0, length, 0, function(err, bytes, buffer) { callback (err, buf3.toString()); }); } ], function (err, result) { if (err) { console.log(err); } else { console.log(result); } });
EXPLAIN
Async is a utility module that detangles the callback spaghetti that especially afflicts
Node developers. It can now be used in the browser, as well as Node, but it’s particularly
useful with Node.
Node developers can install Async using npm:
npm install async
Async provides functionality that we’re now finding in native JavaScript, such as map, filter, and reduce. However, the functionality I want to focus on is its asynchronous control management. The solution used Async’s waterfall(), which implements a series of tasks, passing the results of prior tasks to those next in the queue.
If an error occurs in any task, when the error is passed in the callback to the next task, Async stops the sequence and the error is processed. Comparing the older code and the new Async-assisted solution, the first task is opening a file for writing. In the older code, if an error occurs, it’s printed out. Otherwise, a new Buffer is created and used to write a string to the newly opened file.
In the Async version, though, the functionality to create the file is embedded in a new function openFile(), included as the first element in an array passed to the waterfall() function. The openFile() function takes one parameter, a callback() function, which is called once the file is opened, in the fs.open() callback function and takes as parameters the error object and the file descriptor.
The next task is to write a string to the newly created file. In the old code, this happens directly in the callback function attached to the fs.open() function call. In the Async version, though, writing a string to the file happens in a new function, added as second task to the waterfall() array. Rather than just taking a callback as argument, this function, writerBuffer(), takes the file descriptor fd returned from fs.open(), as well as a callback function.
In the function, after the string is written out to the file using fs.write(), the number of bytes written is captured and passed in the next callback, along with the error and file descriptor. The following task is to write out a second string. Again, in the old code, this happens within the callback function, but this time, the first fs.write()’s callback.
At this time, we’re looking at the third nested callback in the old code, but in the Async version, the second written string operation is just another task and another function in the water fall() task array. The function, writeBuffer2(), accepts the file descriptor, the num‐ ber of bytes written out in the first write task, and, again, a callback function.
Again, it writes the new string out and passes the error, file descriptor, the bytes written out in the first write, and now the bytes written out on the second to the callback function. In the old code within the fourth nested callback function (this one for the second fs.write() function), the count of written bytes is added and used in a call to fs.read() to read in the contents of the newly created file.
The file contents are then output to the console. In the Async modified version, the last task function, readFile(), is added to the task array and it takes a file descriptor, the two writing buffer counts, and a final callback as parameters. In the function, again the two byte counts are added and used in fs.read() to read in the file contents. These contents are passed, with the error object, in the last callback function call.
The results, or an error, are processed in the waterfall()’s own callback function. Rather than a callback nesting four indentations deep, we’re looking at a sequence of function calls in an array, with an absolute minimum of callback nesting.
And we could go on and on, way past the point of what would be insane if we had to use the typical nested callback. I used waterfall() because this control structure implies a series of tasks, each imple‐ mented in turn, and each passing data to the next task.
It takes two arguments:
the task array and a callback with an error and an optional result. Async also supports other control structures such as parallel(), for completing tasks in parallel;
compose(), which creates a function that is a composition of passed functions; and series(), which accomplishes the task in a series but each task doesn’t pass data to the next (as happens with waterfall().
No comments:
Post a Comment