Hemanth.HM

A Computer Polyglot, CLI + WEB ♥'r.

Rethinking Async in Javascript

| Comments

This post is more like a drama script. I would love to see the below conversation as a one-act play on a stage!

Master: Can you write a function to read a contents of a file?

Apprentice: hmm.. that's very easy!

1
2
3
function read(filename){
  return fs.readFileSync(filename, 'utf8');
}

Master: Okies....say the file is like 10GB in size?

Apprentice:

1
2
3
4
5
6
7
8
function read(filename, callback){
  fs.readFile(filename, 'utf8', function (err, res){
    if (err) {
     return callback(err);
    }
    callback(null,res);
  });
}

Master: OK, not bad...now process that file.

Apprentice:


1
2
3
function process(file){
  /* Some processing stuff */
}
1
2
3
4
5
6
7
8
9
10
11
12
function read(filename, callback){
  fs.readFile(filename, 'utf8', function (err, res){
    if (err) {
     return callback(err);
    }
    try {
      callback(null, process(res));
    } catch (ex) {
      callback(ex);
    }
  });
}

1
2
3
4
5
6
7
8
9
10
11
12
13
function read(filename, callback){
  fs.readFile(filename, 'utf8', function (err, res){
    if (err) {
     return callback(err);
    }
    try {
      res = process(res);
    } catch (ex) {
      return callback(ex);
    }
    callback(null, res);
  });
}

Master: Takes a gasp and talks about

how do we avoid callback hell?

  • Name your functions.

  • Keep your code shallow.

  • Modularize!

  • Binding this


Apprentice: Makes a sad face.

Master: When you know...then you know!


Let us make a promise!

  • fulfilled

  • rejected

  • pending

  • settled


1
2
3
4
5
6
7
8
9
var promise = new Promise(function(resolve, reject) {
  // Some async...
  if (/* Allz well*/) {
    resolve("It worked!");
  }
  else {
    reject(Error("It did not work :'("));
  }
});
1
2
3
4
5
promise.then(function(result) {
  console.log(result); // "It worked!"
}, function(err) {
  console.log(err); // Error: "It don not work :'("
});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async1().then(function() {
  return async2();
}).then(function() {
  return async3();
}).catch(function(err) {
  return asyncHeal1();
}).then(function() {
  return asyncHeal4();
}, function(err) {
  return asyncHeal2();
}).catch(function(err) {
  console.log("Ignore them");
}).then(function() {
  console.log("I'm done!");
});

Let us talk about async map:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async.map(['file1','file2','file3'], fs.stat, function(err, results){
    // results is now an array of stats for each file
});

async.filter(['file1','file2','file3'], fs.exists, function(results){
    // results now equals an array of the existing files
});

async.parallel([
    function(){ ... },
    function(){ ... }
], callback);

async.series([
    function(){ ... },
    function(){ ... }
]);

A: Happ(Y)ily ever after?


M: No! We have some more issues:

Streams are broken, callbacks are not great to work with, errors are vague, tooling is not great, community convention is sort of there..


  • you may get duplicate callbacks
  • you may not get a callback at all (lost in limbo)
  • you may get out-of-band errors
  • emitters may get multiple “error” events
  • missing “error” events sends everything to hell
  • often unsure what requires “error” handlers
  • “error” handlers are very verbose
  • callbacks suck

Master: Let us talk about generators:


1
2
3
4
5
6
7
function *Counter(){
 let n = 0;
 while(1<2) {
   yield n;
   n = n + 1;
 }
}
1
2
3
4
5
6
7
8
let CountIter = Counter();

CountIter.next();
// Would result in { value: 0, done: false }

// Again 
CountIter.next();
//Would result in { value: 1, done: false }

1
2
3
4
5
6
7
function *fibonacci() {
    let [prev, curr] = [0, 1];
    for (;;) {
        [prev, curr] = [curr, prev + curr];
        yield curr;
    }
}
1
2
3
4
5
for (fib of fibonacci()) {
    if (fib === 42)
        break;
    console.log(fib);
}

1
2
3
function *powPuff() {
  return Math.pow((yield "x"), (yield "y"));
}
1
2
3
4
5
6
7
let puff = powPuff()

puff.next();

puff.next(2);

puff.next(3); // Guess ;)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* menu(){
  while (true){
    var val = yield null;
    console.log('I ate:', val);
  }
}


let meEat = menu();

meEat.next();

meEat.next("Poori");

meEat.next("Pizza");

meEat.throw(new Error("Burp!"));


1
2
3
4
5
6
7
8
9
10
function* menu(){
  while (true){
    try{
      var val = yield null;
      console.log('I ate: ', val);
    }catch(e){
      console.log('Good, now pay the bill :P');
    }
  }
}
1
meEat.throw(new Error("Burp!"));

M: We can delegate!

1
2
3
4
5
6
7
var inorder = function* inorder(node) {
  if (node) {
    yield* inorder(node.left);
    yield node.label;
    yield* inorder(node.right);
  }
}

A: Confused

M: Deeper you must go! Hmmm


1
2
3
4
5
6
7
function *theAnswer() {
  yield 42;
}

var ans = theAnswer();

ans.next();

A: MORE CONFUSED

M: So....


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function theAnswer() {
  var state = 0;
  return {
    next: function() {
      switch (state) {
        case 0:
          state = 1;
          return {
            value: 42, // The yielded value.
            done: false
          };
        case 1:
          return {
            value: undefined,
            done: true
          };
      }
    }
  };
}

M: But, this has not yet solved the initial issue!


M: Let us assume a function named run ~_~


1
2
3
4
5
6
7
8
9
run(genFunc){
 /*
   _ __ ___ __ _ __ _(_) ___ 
 | '_ ` _ \ / _` |/ _` | |/ __|
 | | | | | | (_| | (_| | | (__ 
 |_| |_| |_|\__,_|\__, |_|\___|
                  |___/        
 */
}
1
2
3
4
5
6
run(function *(){
  var data = yield read('package.json');
  var result = yield process(data);
  console.log(data);
  console.log(result);
});


1
2
3
4
5
6
7
8
9
10
11
12
13
var fs = require(fs);

function run(fn) {
  var gen = fn();

  function next(err, res) {
    var ret = gen.next(res);
    if (ret.done) return;
    ret.value(next);
  }

  next();
}

A: WOW!

M: HMM, let us talk about THUNKS!

A: Thunks??!


let timeoutThunk = (ms) => (cb) => setTimeout(cb,ms)


1
2
3
4
5
function readFile(path) {
    return function(callback) {
        fs.readFile(path, callback);
    };
}

Instead of :

1
readFile(path, function(err, result) { ... });

We now have:

1
readFile(path)(function(err, result) { ... });

So that:

1
var data = yield read('package.json');

Master: Baaazinga!


M: More usecases! Generator-based flow control.

A: Very eager.


1
$ npm install thunkify

Turn a regular node function into one which returns a thunk!

1
2
3
4
5
6
7
8
var thunkify = require('thunkify');
var fs = require('fs');

var read = thunkify(fs.readFile);

read('package.json', 'utf8')(function(err, str){

});

1
$ npm install co

Write non-blocking code in a nice-ish way!

1
2
3
4
var co = require('co');
var thunkify = require('thunkify');
var request = require('request');
var get = thunkify(request.get);
1
2
3
4
5
6
7
8
co(function *(){
  try {
    var res = yield get('http://badhost.invalid');
    console.log(res);
  } catch(e) {
    console.log(e.code) // ENOTFOUND
 }
}());

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var urls = [/* Huge list */];

// sequential

co(function *(){
  for (var i = 0; i < urls.length; i++) {
    var url = urls[i];
    var res = yield get(url);
    console.log('%s -> %s', url, res[0].statusCode);
  }
})()

// parallel

co(function *(){
  var reqs = urls.map(function(url){
    return get(url);
  });

  var codes = (yield reqs).map(function(r){ return r.statusCode });

  console.log(codes);
})()

M: Interesting? What more?


1
$ npm install co-sleep
1
2
3
4
5
6
7
8
9
var sleep = require('co-sleep');
var co = require('co');

co(function *() {
  var now = Date.now();
  // wait for 1000 ms
  yield sleep(1000);
  expect(Date.now() - now).to.not.be.below(1000);
})();

1
$ npm install co-ssh
1
2
3
4
5
6
7
8
9
10
11
12
var ssh = require('co-ssh');

var c = ssh({
  host: 'n.n.n.n',
  user: 'myuser',
  key: read(process.env.HOME + '/.ssh/some.pem')
});

yield c.connect();
yield c.exec('foo');
yield c.exec('bar');
yield c.exec('baz');

1
2
3
4
5
var monk = require('monk');
var wrap = require('co-monk'); // co-monk!
var db = monk('localhost/test');

var users = wrap(db.get('users'));
1
2
3
yield users.remove({});

yield users.insert({ name: 'Hemanth', species: 'Cat' });
1
2
3
4
5
6
// Par||el!
yield [
  users.insert({ name: 'Tom', species: 'Cat' }),
  users.insert({ name: 'Jerry', species: 'Rat' }),
  users.insert({ name: 'Goffy', species: 'Dog' })
];

1
$ npm install suspend
1
2
3
4
5
6
7
var suspend = require('suspend'),
    resume = suspend.resume;

suspend(function*() {
    var data = yield fs.readFile(__filename, 'utf8', resume());
    console.log(data);
})();
1
2
3
4
5
6
var readFile = require('thunkify')(require('fs').readFile);

suspend(function*() {
    var package = JSON.parse(yield readFile('package.json', 'utf8'));
    console.log(package.name);
});

A: Is that all? Can I go home now?

M: No!?


M: Koa FTW?!


1
$ npm install koa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var koa = require('koa');
var app = koa();

// logger

app.use(function *(next){
  var start = new Date;
  yield next;
  var ms = new Date - start;
  console.log('%s %s - %s', this.method, this.url, ms);
});

// response

app.use(function *(){
  this.body = 'Hello World';
});

app.listen(3000);

A: Something for the client?


M: Hmmm, good question, we have Task.js

generators + promises = tasks

1
2
3
4
5
6
7
8
9
10
11
12
13
<script type="application/javascript" src="task.js"></script>

<!-- 'yield' and 'let' keywords require version opt-in -->
<script type="application/javascript;version=1.8">
function hello() {
    let { spawn, sleep } = task;
    spawn(function() { // Firefox does not yet use the function* syntax
        alert("Hello...");
        yield sleep(1000);
        alert("...world!");
    });
}
</script>

M: Sweet and simple!

1
2
3
4
5
6
7
8
9
10
spawn(function*() {
    try {
        var [foo, bar] = yield join(read("foo.json"),
                                    read("bar.json")).timeout(1000);
        render(foo);
        render(bar);
    } catch (e) {
        console.log("read failed: " + e);
    }
});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var foo, bar;
var tid = setTimeout(function() { failure(new Error("timed out")) }, 1000);

var xhr1 = makeXHR("foo.json",
                   function(txt) { foo = txt; success() },
                   function(err) { failure() });
var xhr2 = makeXHR("bar.json",
                   function(txt) { bar = txt; success() },
                   function(e) { failure(e) });

function success() {
    if (typeof foo === "string" && typeof bar === "string") {
        cancelTimeout(tid);
        xhr1 = xhr2 = null;
        render(foo);
        render(bar);
    }
}

function failure(e) {
    xhr1 && xhr1.abort();
    xhr1 = null;
    xhr2 && xhr2.abort();
    xhr2 = null;
    console.log("read failed: " + e);
}

A: Thank you master I feel enlightened!

M: Are you sure?

A: Hmmm....

M: This is just the beginning!


M: Let me talk about async-await

1
2
3
4
5
async function <name>?<argumentlist><body>

=>

function <name>?<argumentlist>{ return spawn(function*() <body>); }

Example of animating elements with Promise:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function chainAnimationsPromise(elem, animations) {
    var ret = null;
    var p = currentPromise;
    animations.forEach(function(anim){
      p = p.then(function(val) {
            ret = val;
            return anim(elem);
        });
    });

    return p.catch(function(e) {
        /* ignore and keep going */
    }).then(function() {
        return ret;
    });
}

Same example with task.js:

1
2
3
4
5
6
7
8
9
10
11
function chainAnimationsGenerator(elem, animations) {
    return spawn(function*() {
        var ret = null;
        try {
            for(var anim of animations) {
                ret = yield anim(elem);
            }
        } catch(e) { /* ignore and keep going */ }
        return ret;
    });
}

Same example with async/await:

1
2
3
4
5
6
7
8
9
async function chainAnimationsAsync(elem, animations) {
    var ret = null;
    try {
        for(var anim of animations) {
            ret = await anim(elem);
        }
    } catch(e) { /* ignore and keep going */ }
    return ret;
}

Another example from the draft:

1
2
3
4
5
6
7
8
9
async function getData() {
  var items = await fetchAsync('http://example.com/users');
  return await* items.map(async(item) => {
    return {
      title: item.title,
      img: (await fetchAsync(item.userDataUrl)).img
    }
  }
}

M: Now let us do a performance review.

A: Runs away!!


Hope you liked the play! You might also like reading Are you async yet? post.

Comments