View
2.998
Download
1
Category
Preview:
DESCRIPTION
This talk was given at JSSummit 2013. Entitled "Avoiding Callback Hell with Async.js", my talk focused on common pitfalls with asynchronous functions and callbacks in JavaScript, and using the async.js library and its advanced control flows to create cleaner, more manageable code.
Citation preview
Avoiding Callback Hell with Async.js
C. Aaron Cois, Ph.D. www.codehenge.net
Sup
@aaroncois
www.codehenge.net
github.com/cacois
So, JavaScript?
What’s cool?• Robust event model• Asynchronous programming• Client-side and Server-side
What’s cool?• Robust event model• Asynchronous programming• Client-side and Server-side
What can cause headaches?
Callbacks
JavaScript uses callback functions to handle asynchronous control flow
Anatomy of a Callback fs = require('fs'); fs.readFile('f1.txt','utf8',function(err,data){ if (err) { return console.log(err); } console.log(data); });
Anatomy of a Callback fs = require('fs'); fs.readFile('f1.txt','utf8',function(err,data){ if (err) { return console.log(err); } console.log(data); });
Anonymous, inline callback
Anatomy of a Callback fs = require('fs'); fs.readFile('f1.txt','utf8', function(err,data){ if (err) { return console.log(err); } console.log(data); } );
Equivalentformatting
Callback Hell
When working with callbacks, nesting can get quite out of hand…
Callback Hellfunc1(param, function(err, res) { func2(param, function(err, res) { func3(param, function(err, res) { func4(param, function(err, res) { func5(param, function(err, res) { func6(param, function(err, res) { func7(param, function(err, res) { func8(param, function(err, res) { func9(param, function(err, res) { // Do something… }); }); }); }); }); }); }); });});
Callback Hellfunc1(param, function(err, res) { func2(param, function(err, res) { func3(param, function(err, res) { func4(param, function(err, res) { func5(param, function(err, res) { func6(param, function(err, res) { func7(param, function(err, res) { func8(param, function(err, res) { func9(param, function(err, res) { // Do something… }); }); }); }); }); }); }); });});
Best case, this is linearfunc1
func2
func3
. . .
func9
But it can branchfunc1
func2
func3
. . .
func9
func2
func3func3 func3
. . .
func9
. . .
func9
. . .
func9
But it can branchfunc1
func2
func3
. . .
func9
func2
func3func3 func3
. . .
func9
. . .
func9
. . .
func9
… func2(param, function(err, results) { _.each(results, func3(param, function(err, res) { func4(param, function(err, res) { … }); } });});
Some specific challenges• When branching, we can’t know the
order of these function calls• If we want parallel execution, we
have to do some real gymnastics to get return data back together
• Also, scoping becomes a challenge
A more ‘real’ examplevar db = require('somedatabaseprovider');//get recent postshttp.get('/recentposts', function(req, res) { // open database connection db.openConnection('host', creds,function(err, conn){ res.param['posts'].forEach(post) { conn.query('select * from users where id='+post['user'],function(err,users){ conn.close(); res.send(users[0]); }); } });});
A more ‘real’ examplevar db = require('somedatabaseprovider');//get recent postshttp.get('/recentposts', function(req, res) { // open database connection db.openConnection('host', creds,function(err, conn){ res.param['posts'].forEach(post) { conn.query('select * from users where id='+post['user'],function(err,users){ conn.close(); res.send(users[0]); }); } });});
Solutions• You can make this easier to read by
separating anonymous functions
• Passing function references instead of anonymous functions helps even more
Inline callback fs = require('fs'); fs.readFile('f1.txt','utf8',function(err,data){ if (err) { return console.log(err); } console.log(data); });
Separate Callback fs = require('fs'); callback = function(err,data){ if (err) { return console.log(err); } console.log(data); }
fs.readFile('f1.txt','utf8',callback);
Can turn this: var db = require('somedatabaseprovider');
http.get('/recentposts', function(req, res){ db.openConnection('host', creds, function(err,
conn){ res.param['posts'].forEach(post) { conn.query('select * from users where id=' +
post['user'],function(err,results){ conn.close(); res.send(results[0]); }); } });});
…into this var db = require('somedatabaseprovider'); http.get('/recentposts', afterRecentPosts); function afterRecentPosts(req, res) { db.openConnection('host', creds, function(err, conn) { afterDBConnected(res, conn); }); } function afterDBConnected(err, conn) { res.param['posts'].forEach(post) { conn.query('select * from users where id='+post['user'],afterQuery); } } function afterQuery(err, results) { conn.close(); res.send(results[0]); }
Good start!• Callback function separation is a nice
aesthetic fix• The code is more readable, and thus
more maintainable• But it doesn’t improve your control
flow– Branching and parallel execution are still
problems
Enter Async.js
Async.js provides common patterns for asyncronous code control flow
https://github.com/caolan/async
BONUS: Also provides some common functional programming paradigms
Client or Server -sideMy examples will mostly be Node.js code, but Async.js can be used in both client and server side code
Serial/Parallel ExecutionRun functions in series…
…or parallel
Function 1
Function 2
Function 3
Function 4
Function 1
Function 2
Function 3
Function 4
Serial/Parallel Functions async.parallel([ function(){ ... }, function(){ ... } ], callback); async.series([ function(){ ... }, function(){ ... } ]);
async.parallel([ function(){ ... }, function(){ ... } ], callback); async.series([ function(){ ... }, function(){ ... } ]);
Serial/Parallel Functions
Single Callback!
Waterfall ExecutionAsync also provides a flow for serial execution, passing results to successive functions
Function 1
Function 2
Function 3
Function 4
args args args
Waterfall async.waterfall([ function(){ callback(arg1); }, function(arg1) { callback(ar2,ar3) }, function(arg1, arg2){ callback(“done”) } ], function(err, results){ // results now equals “done” });
TimesTimes() offers a shortcut to iterating over a function multiple times in parallel
async.times(5, function(n, next) { createUser(n,function(err, user { next(err, user); }) }, function(err, users){ // ‘users’ now contains 5 users });
Let’s see some code…
Collection ManagementFunctional programming provides some useful tools that are becoming mainstream
Specifically, map, reduce, and filter operations are now common in many languages
Map
The Map operation calls a given function on each item of a list
Map async.map([‘file1’,‘file2’,‘file3’], funct, callback); async.mapSeries([‘file1’,‘file2’], funct, callback);
async.mapLimit([‘file1’,‘file2’,‘file3’], limit, funct, callback);
Reduce
The Reduce operation aggregates a list of values into a single result, using a specified aggregation function
Reduce async.reduce([val1,val2,…],memo, function(memo,item,cb){ // doStuff }); async.reduceRight([val1,val2],memo, function(memo,item,cb){ // doStuff });
Initial result state
FilterTo minimize computational overhead, it’s often helpful to filter data sets to only operate on acceptable values
Async.js provides a Filter function to do just this
Filter async.filter([‘file1’,‘file2’,‘file3’], fs.exists, function(results){ // results is now an array // of files that exist });
In SummationAsync.js provides:
• Powerful asynchronous control flows• Functional programming tools
Write clean code, live the dream.
Thanks!
Any questions?
@aaroncois
Recommended