Avoiding Callback Hell with Async.js

download Avoiding Callback Hell with Async.js

of 43

Embed Size (px)

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.

Transcript of Avoiding Callback Hell with Async.js

  • Avoiding Callback Hell with Async.js C. Aaron Cois, Ph.D. www.codehenge.net
  • Sup www.codehenge.net @aaroncois github.com/cacois
  • So, JavaScript?
  • Whats cool? Robust event model Asynchronous programming Client-side and Server-side
  • Whats 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); } ); Equivalent formatting
  • Callback Hell When working with callbacks, nesting can get quite out of hand
  • Callback Hell func1(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 Hell func1(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 linear func1 func2 func3 ... func9
  • But it can branch func1 func2 func2 func3 func3 func3 func3 ... ... ... ... func9 func9 func9 func9
  • But it can branch func2(param, function(err, results) { _.each(results, func3(param, function(err, res) { func4(param, function(err, res) { }); } }); }); func1 func2 func2 func3 func3 func3 func3 ... ... ... ... func9 func9 func9 func9
  • Some specific challenges When branching, we cant 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 example var db = require('somedatabaseprovider'); //get recent posts http.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 example var db = require('somedatabaseprovider'); //get recent posts http.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 doesnt 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 -side My examples will mostly be Node.js code, but Async.js can be used in both client and server side code
  • Serial/Parallel Execution Run functions in series Function 1 Function 2 Function 3 Function 4 Function 2 Function 3 Function 4 or parallel Function 1
  • Serial/Parallel Functions async.parallel([ function(){ ... }, function(){ ... } ], callback); async.series([ function(){ ... }, function(){ ... } ]);
  • Serial/Parallel Functions async.parallel([ function(){ ... }, function(){ ... } ], callback); async.series([ function(){ ... }, function(){ ... } ]); Single Callback!
  • Waterfall Execution Async also provides a flow for serial execution, passing results to successive functions Function 1 args args args Function 2 Function 3 Function 4
  • Waterfall async.waterfall([ function(){ callback(arg1); }, function(arg1) { callback(ar2,ar3) }, function(arg1, arg2){ callback(done) } ], function(err, results){ // results now equals done });
  • Times Times() 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 });
  • Lets see some code
  • Collection Management Functional 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 Initial result state async.reduce([val1,val2,],memo, function(memo,item,cb){ // doStuff }); async.reduceRight([val1,val2],memo, function(memo,item,cb){ // doStuff });
  • Filter To minimize computational overhead, its 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 Summation Async.js provides: Powerful asynchronous control flows Functional programming tools