Async What?

An Introduction To Async Control Flow

Wes Todd

@wesleytodd on the GitHubs and Twitters

I work @Vube

Wes in the oval

We do important stuff

This meetup is awesome!

Wes in the Factory

Thanks Johnathan and the Capital Factory!

Why Async?

Javascript is single threaded.

That means Javascript can only do one thing at a time.

What does that look like?

Um....not like that

More like this...

Thanks to Richard Key for the diagram.

Callbacks Are Great!

Data Getter Thingy

function myDataGetterThingy(done) {
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if (xhr.readyStateChange === 4) {
            try {
                var res = JSON.parse(xhr.responseText);
  
                // Handel an error status
                if (xhr.status !== 200) {
                    return done(xhr.status, res);
                }
  
                // Successful response
                done(null, res);
            } catch(e) {
                // JSON parse error
                done(e, xhr.responseText);
            }
        }
    };
    xhr.onerror = function() {
        done('Request failed');
    };
    xhr.open('GET', 'foo.json', true);
    xhr.send(null);
}

Maybe something more simple to start

function myAsyncFunc(done) {
    // Do Stuff!!
    if (check === errorCondition) {
        // Oops!!
        return done(new Error());
    }
    // No errors...
    done(null, 'Result');
}

Callbacks Are Great!

Add Another Async Thingeeeee

function anotherAsyncee(done) {
  done();
}

And do one thing after

myDataGetterThingy(function(err, res) {
    if (err) {
        return console.error(err);
    }
    anotherAsyncee(function(err) {
        if (err) {
            return console.error(err);
        }
        console.log('Success!!!');
    });
});

And Another...

myDataGetterThingy(function(err, res) {
    if (err) {
        return console.error(err);
    }
    anotherAsyncee(function(err) {
        if (err) {
            return console.error(err);
        }
        anotherAsyncee(function(err) {
            if (err) {
                return console.error(err);
            }
            console.log('Success!!!');
        });
    });
});

And Another...

myDataGetterThingy(function(err, res) {
    if (err) {
        return console.error(err);
    }
    anotherAsyncee(function(err) {
        if (err) {
            return console.error(err);
        }
        anotherAsyncee(function(err) {
            if (err) {
                return console.error(err);
            }
            anotherAsyncee(function(err) {
                if (err) {
                    return console.error(err);
                }
                console.log('Not feeling so successful anymore are ya?');
            });
        });
    });
});

Now lets talk concurrency

var complete = 0;

function done(err, res) {
    if (err) {
        return console.error(err);
    }
    complete++;
    if (complete === 2) {
        console.log('Success!!!');
    }
}

myDataGetterThingy(done);
anotherAsyncee(done);

Why does this have to suck soooo bad?

Tools

  • Async.js
  • jQuery Deferred
  • Q
  • RSVP.js
  • Angular $q service

Async.js

2nd most depended on NPM Module

  • series, parallel and waterfall
  • each, eachSeries and map
  • detect, cargo and memorize

Doing Two Things With Async.js

var async = require('async');

async.series([asyncFnc1, asyncFnc2], function(err, res) {
    if (err) {
        return console.error(err);
    }
    console.log('Success!!!');
});

async.parallel([asyncFnc1, asyncFnc2], function(err, res) {
    if (err) {
        return console.error(err);
    }
    console.log('Success!!!');
});

Im gonna use Async for all the things!!

    async.series = function (tasks, callback) {
    callback = callback || function () {};
    if (_isArray(tasks)) {
        async.mapSeries(tasks, function (fn, callback) {
            if (fn) {
                fn(function (err) {
                    var args = Array.prototype.slice.call(arguments, 1);
                    if (args.length <= 1) {
                        args = args[0];
                    }
                    callback.call(null, err, args);
                });
            }
        }, callback);
    }
    else {
        var results = {};
        async.eachSeries(_keys(tasks), function (k, callback) {
            tasks[k](function (err) {
                var args = Array.prototype.slice.call(arguments, 1);
                if (args.length <= 1) {
                    args = args[0];
                }
                results[k] = args;
                callback(err);
            });
        }, function (err) {
            callback(err, results);
        });
    }
};
async.eachSeries = function (arr, iterator, callback) {
    callback = callback || function () {};
    if (!arr.length) {
        return callback();
    }
    var completed = 0;
    var iterate = function () {
        iterator(arr[completed], function (err) {
            if (err) {
                callback(err);
                callback = function () {};
            }
            else {
                completed += 1;
                if (completed >= arr.length) {
                    callback();
                }
                else {
                    iterate();
                }
            }
        });
    };
    iterate();
};
var doSeries = function (fn) {
   return function () {
       var args = Array.prototype.slice.call(arguments);
       return fn.apply(null, [async.eachSeries].concat(args));
   };
};
var _asyncMap = function (eachfn, arr, iterator, callback) {
    arr = _map(arr, function (x, i) {
        return {index: i, value: x};
    });
    if (!callback) {
        eachfn(arr, function (x, callback) {
            iterator(x.value, function (err) {
                callback(err);
            });
        });
    } else {
        var results = [];
        eachfn(arr, function (x, callback) {
            iterator(x.value, function (err, v) {
                results[x.index] = v;
                callback(err);
            });
        }, function (err) {
            callback(err, results);
        });
    }
};
async.mapSeries = doSeries(_asyncMap);

Rolling Back...to Callbacks

See the Pen Async Patterns Without A Library by Wes Todd (@wesleytodd) on CodePen.

Look Ma, I can do JS Good!!!

Promises (Q/RSVP/Angular $q)

People rave about their promises.

But for the past few years you couldn't use them without a library wrapping all your functions.

And on top of it, the Promises people didn't agree on an implementation.

Spoiler Alert

They did end up agreeing on Promises/A+

Angular $q Service

angular.module('SocialNetworkForCats')
.service('fancyFeast', ['$q', '$rootScope', function($q, $rootScope) {

    this.openCan = function() {
        var dfd = $q.defer();

        // Show can opening

        // Can opened
        $rootScope.$on('canOpen', function() {
            dfd.resolve();
        });

        // Can errored out while opening
        $rootScope.$on('cutOpenHandAndBleedAllOver', function() {
            dfd.reject('MEMORY_LEAK');
        });
        return dfd.promise;
    };

}]);
// ...

this.dumpInADirtyBowl = function() {
    var dfd = $q.defer();
    $rootScope.$on('bowlFull', function() {
        dfd.resolve();
    });
    $rootScope.$on('bowlOverflowedAndYouGotMad', function() {
        dfd.reject('BUFFER_OVERFLOW');
    });
    return dfd.promise;
};

this.giveToCats = function() {
    // ...
};

this.serveFood = function() {
    return this.openCan()
        .then(this.dumpInADirtyBowl)
        .then(this.giveToCats);
};

// ...

Promises Make This Easy

fancyFeast.serveFood()
    .then(function() {
        console.log('Your dinner is served!');
    }, function(err) {
        if (err === 'MEMORY_LEAK') {
            console.log('Call 911....Im bleeding out!!!');
        } else if (err === 'BUFFER_OVERFLOW') {
            console.log('Screw it, you cats can feed yourselves.');
        } else if (err === 'OBJ_NULL') {
            console.log('You dont own any cats...');
        }
    });

Promises Are The Future

ES6 Promises

var promise = new Promise(function(resolve, reject) {
    if (check === errorCondition) {
        return reject(new Error());
    }
    resolve('Success!!');
});

promise.then(function() {
    console.log('Success');
}, function(err) {
    console.err(err);
});
function myDataGetterThingy() {
    return new Promise(function(resolve, reject) {
        var xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function() {
            if (xhr.readyStateChange === 4) {
                try {
                    var res = JSON.parse(xhr.responseText);
  
                    // Error status
                    if (xhr.status !== 200) {
                        return reject(res);
                    }
  
                    // Successful response
                    resolve(res);
                } catch(e) {
                    // JSON parse error
                    reject(e);
                }
            }
        };
        xhr.onerror = function() {
            reject('Request failed');
        };
        xhr.open('GET', 'foo.json', true);
        xhr.send(null);
    });
}

Using this is way eaiser....

// In series
myDataGetterThingy()
    .then(function(res) {
        // do stuff with the response
        return anotherAsyncee();
    })
    .then(function(res) {
        console.log('Success!!!');
    })
    .catch(function(err) {
        console.error(err);
    });

// In parallel
Promise.all([myDataGetterThingy(), anotherAsyncee()])
    .then(function() {
        console.log('Success!!!');
    })
    .catch(function(err) {
        console.error(err);
    });
Learn More Here

Resources

Questions?

Go Have Fun!

#wesvisits images courtesy of Alan