• Stars
    star
    102
  • Rank 335,584 (Top 7 %)
  • Language
    JavaScript
  • License
    Apache License 2.0
  • Created about 11 years ago
  • Updated over 10 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A showcase of different ways to handle asynchronous control flow in Javascript.

Async JS Control Flow Showcase

This is a project designed to showcase different ways to handle asynchronous control flow in Javascript. If you're familiar with this concept, you might want to skip to the list of projects or the description of callback issues.

Note that this is meant to be an informational, not a competitive, guide to all the approaches to flow control on offer.

Background

Javascript applications, including Node.js-based software, are most frequently structured around event-driven or asynchronous methods. It's possible that a function foo or bar could kick off some logic that is not yet complete when the functions themselves complete. One common example of this is Javascript's setTimeout(). setTimeout, as the name implies, doesn't actually pause execution for a designated amount of time. Instead, it schedules execution for a later time, and meanwhile returns control to the script:

var done = function() {
    console.log("set timeout finished!");
};

setTimeout(done, 1000);
console.log("hello world");

The output of this script is:

hello world
set timeout finished!

Callbacks

Node.js has generalized this asynchronous control flow idea through the use of callbacks. This is a convention whereby an asynchronous function takes a parameter which is itself a function. This function, called a callback, is then called whenever the asynchronous result is ready. In Node, callbacks are called with the convention that an Error object is passed as the first parameter for use in error handling, and further results are sent as subsequent parameters. Let's take a look at an example:

var fs = require('fs');

var readFileCallback = function(err, fileData) {
    if (err) {
        console.log("We got an error! Oops!");
        return;
    }
    console.log(fileData.toString('utf8'));
};

fs.readFile('/path/to/my/file', readFileCallback);

In this example, we use a callback-based method fs.readFile() to get the contents of a file. The function readFileCallback is called when the file data has been retrieved. If there was an error, say because the file doesn't exist, we get that as the first parameter, and can log a helpful message. Otherwise, we can display the file contents.

Because we can use anonymous functions, it's much more common to see code that looks like this:

var fs = require('fs');

fs.readFile('/path/to/my/file', function(err, fileData) {
    if (err) {
        console.log("We got an error! Oops!");
        return;
    }
    console.log(fileData.toString('utf8'));
});

This architecture can be quite powerful because it is non-blocking. We could easily read two files at the same time just by putting calls to fs.readFile one after the other:

var fs = require('fs');

var readFileCallback = function(err, fileData) {
    if (err) {
        console.log("We got an error! Oops!");
        return;
    }
    console.log(fileData.toString('utf8'));
};

fs.readFile('/path/to/my/file', readFileCallback);
fs.readFile('/path/to/another/file', readFileCallback);

In this example, the file contents will be printed out in an undetermined order because they're kicked off at roughly the same time, and their callbacks will be called when the system is done reading each one, which could vary based on many factors.

The impact of callbacks

There are a number of issues that arise as part of a callback-based architecture. These issues range from the aesthetic to the practical (e.g., some argue callbacks lead to less readable or less maintainable code).

Rightward drift

It often happens that you want to run a number of asynchronous methods, one after the other. In this case, each method must be called in the callback of the previous method. Using the anonymous function strategy detailed above, you end up with code that looks like this:

asyncFn1(function() {
    asyncFn2(function() {
        asyncFn3(function() {
            asyncFn4(function() {
                console.log("We're done!");
            });
        });
    });
});

To some people, once you start filling these functions out with their own particular logic, it's very easy to lose track of where you are in the logical flow.

Branching logic

Sometimes you might want to call a function bar() only if the result of another function foo() matches some criteria. If these are synchronous functions, the logic looks like this:

var res = foo();
if (res === "5") {
    res = bar(res);
}
res = baz(res);
console.log("After transforming, res is " + res);

If foo and bar are asynchronous functions, however, it gets a little more complicated. One option is to duplicate code:

foo(function(res) {
    if (res === "5") {
        bar(res, function(res2) {
            baz(res2, function(res3) {
                console.log("After transforming, res is " + res3);
            });
        });
        return;
    }
    baz(res, function(res2) {
        console.log("After transforming, res is " + res2);
    });
});

In this case, we've duplicated the calls to baz. An alternative is to create a next function that encapsulates the baz call and subsequent log statement:

var next = function(res) {
    baz(res, function(res2) {
        console.log("After transforming, res is " + res2);
    });
};

foo(function(res) {
    if (res === "5") {
        bar(res, function(res2) {
            next(res2);
        });
        return;
    }
    next(res);
});

This is more DRY, but at the cost of creating a function whose only purpose is to continue the logical flow of the code, called in multiple places.

Looping

If you need to perform an async method on a number of objects, it can be a little mind-bending. Synchronously, we can do something like this:

var collection = [objA, objB, objC];
var response = [];
for (var i = 0; i < collection.length; i++) {
    response.push(transformMyObject(collection[i]));
}
console.log(response);

If transformMyObject is actually asynchronous, and we need to transform each object one after the other, we need to do something more like this:

var collection = [objA, objB, objC];
var response = [];
var doTransform = function() {
    var obj = collection.unshift();
    if (typeof obj === "undefined") {
        console.log(response);
    } else {
        transformMyObject(obj, function(err, newObj) {
            response.push(newObj);
            doTransform();
        });
    }
};
doTransform();

Error handling

You can't use Javascript's basic error handling techniques (try/catch) to handle errors in callbacks, even if they're defined in the same scope. In other words, this doesn't work:

var crashyFunction = function(cb) {
    throw new Error("uh oh!");
};

var runFooBar = function(cb) {
    foo(function() {
        crashyFunction(function() {
            bar(function() {
                cb();
            });
        });
    });
};

try {
    runFooBar(function() {
        console.log("We're done");
    });
} catch (err) {
    console.log(err.message);
}

This is why Node.js uses a convention of passing errors into callbacks so they can be handled:

var crashyFunction = function(cb) {
    try {
        throw new Error("uh oh!");
    } catch (e) {
        cb(e);
    }
};

var runFooBar = function(cb) {
    foo(function(err) {
        if (err) return cb(err);
        crashyFunction(function(err) {
            if (err) return cb(err);
            bar(function(err) {
                if (err) return cb(err);
                cb();
            });
        });
    });
};

runFooBar(function(err) {
    if (err) return console.log(err.message);
    console.log("We're done!");
});

As you can see, the result of this is that we have to check the error state in every callback so we can short-circuit the chain and pass the error to the top-level callback. This is a bit redundant at best.

Node.js now has domains, which makes this problem a little easier to handle.

Alternatives to callbacks

Of course, callbacks aren't the only way to do asynchronous control flow. There are many helpful libraries that make using callbacks less prone to the issues above. There are also alternatives which aren't callback-based at all, though they might rely on callbacks under the hood. The point of this project is to enable a side-by-side comparison of these approaches.

The callbacks directory of this repo has a number of reference Javascript files which demonstrate in code how callback-based flow control works. These files can all be run using Node.js.

The following projects have implemented, or are in the process of implementing, revisions of the reference scripts using their particular approach to asynchronous control flow. Please check them out, read the directory's README to see how they address the issues above, run the code, and make an informed decision about which approach to adopt in your own projects!

Hint: take a look at the "kitchen sink" example file in each of the projects to see how they handle all the issues in one script.

  • Callbacks (i.e., the "standard" or "naive" approach above)

Other approaches:

Bitdeli Badge

More Repositories

1

simple-wd-spec

A simplified guide to the W3C WebDriver spec
120
star
2

yiewd

Wd.js wrapper that uses Generators for cleaner code
JavaScript
93
star
3

sausage

A PHP framework for the Sauce Labs REST API
PHP
69
star
4

tmux-safekill

A tmux plugin that attempts to safely kill processes before exiting a tmux session
Shell
61
star
5

appium-book

Appium: Mobile Automation Made Awesome (the book)
TeX
42
star
6

monocle-js

Library for using a readable blocking-like syntax with event-driven Javascript code
JavaScript
33
star
7

asyncbox

A collection of ES7 async/await utilities
JavaScript
24
star
8

appium-raspi-driver

Appium driver for Raspberry PI GPIO
JavaScript
23
star
9

appium-ocr-plugin

Tesseract-based OCR plugin for Appium
TypeScript
20
star
10

hmm-tagger

Hidden Markov Model POS Tagger
Python
19
star
11

sauce-node-demo

Demo of how to use Sauce OnDemand with Node.js, Express.js, Vows.js, Wd, etc...
JavaScript
16
star
12

chrome-ops

Helper library for dealing with Chromedriver performance logs
JavaScript
14
star
13

paraunit

A parallel wrapper for PHPUnit
PHP
13
star
14

mochawait

Mocha wrapper that provides an interface for ES7 async/await
JavaScript
11
star
15

triager

A server that automatically assigns team members to new issues to triage
JavaScript
10
star
16

unity-plugin-workshop

JavaScript
8
star
17

wd-series

Async wrapper for admc/wd (the Node WebDriver client).
JavaScript
7
star
18

appiumconf2019

Code for my AppiumConf 2019 Appium + Circuit Playground Express + RaspberryPi Demo
JavaScript
7
star
19

mapify

Convert JS Objects to ES6 Maps and vice versa
JavaScript
6
star
20

appium-ruby-example

Code for the Appium + Sauce + Ruby guide
Ruby
6
star
21

appium-grid-example

Example of using Appium 2 with Selenium Grid 4
JavaScript
5
star
22

runsauce

Run a simple test with any Sauce options
JavaScript
5
star
23

sausage-bun

A wrapper script that checks system readiness for Sausage and gets it all installed
PHP
5
star
24

node-binary-cookies

Binary cookies parser for Node
JavaScript
4
star
25

oneword

An app that lets viewers of a presentation choose a word to show in a cloud
JavaScript
3
star
26

campfire-tunes

A Mac OS X app that puts your currently playing iTunes or Spotify songs and album art into a Campfire room
Objective-C
3
star
27

sauce-php-demo

Sauce OnDemand + PHP demo (in YII)
PHP
3
star
28

demoize

Show code that's being executed while hiding the code that tells us to execute it.
CSS
2
star
29

automation-guild-2019

Sample code for my 2019 Automation Guild presentation
Java
2
star
30

underdark

D&D blog and podcast site
JavaScript
2
star
31

appium-fs-driver

Demo filesystem driver for Appium (not for real use)
JavaScript
2
star
32

jlipps.com

My personal site
JavaScript
2
star
33

dotfiles

My dotfiles
TeX
1
star
34

testng-demo

Automated test demo
Java
1
star
35

echo-daemon

Python
1
star
36

sauce-js-challenge

Sauce Labs interview
1
star
37

vim-bda

Simple VIM plugin to delete all open buffers
Vim Script
1
star
38

mobile-dev-test-examples

Code samples for Mobile Dev+Test 2017
JavaScript
1
star
39

gnt-nlp

A Natural Language Processing library for the Greek New Testament
JavaScript
1
star
40

appium-orchestra

Appium automating all the musical things
JavaScript
1
star
41

jonathanlipps

The Homepage of Jonathan Lipps
CoffeeScript
1
star