Making Asynchronous Code Synchronous In Javascript

Well I have been guilty of not exploring ES6, and even the ES7 standard was already finalized!

When working with complex asynchronous logics, I use promises to manage them. But for simple tasks, I’d still prefer callbacks, which I find to be very readable, if you know how to manage them. In ES6 however, using the yield keyword and generators, you could make asynchronous code look very much like synchronous one. Consider the following example using co and thunkify modules:

'use strict';

let thunkify = require('thunkify'),
    co = require('co');

var timeout = thunkify((ms, cb) => {
  setTimeout(() => { cb(null, 'after ' + ms); }, ms);
});

co(function* () {
  console.log('start');
  let first = yield timeout(2000);
  console.log(first);
  let second = yield timeout(100);
  console.log(second);
  return 'done';
}).then(console.log, console.error);

The output is:

start
after 2000
after 100
done

Where the first call to timeout will finish first, before the second one is executed. Outside the co() block however, code would still run normally. Apart from the yield keywords, everything looks like synchronous code.

My Take on a co like Function

The Runner Block

So for the sake of understanding how generator works, I write something similar to co() function, which works like the following:

run(function* () {
  console.log('runner start');

  let ms1 = yield timeout(600);
  console.log('>> timeout', ms1);

  // try passing an invalid URL here, see how it will stop the runner
  // when an error is thrown
  try {
    let json = yield get('http://nodejs.org/dist/index.json');
    console.log('>> get %s ...', JSON.stringify(json).replace(/\s*/g, '').substr(0, 100));
  } catch (e) {
    console.error('>> get', e);
  }

  let ms2 = yield timeout(500);
  console.log('>> timeout', ms2);

  console.log('runner end');
});

Generator Function for Asynchronous Task

Instead of using thunkify, I use native generator functions for timeout and get, like:

function *timeout(ms) {
  const caller = yield;

  setTimeout(() => {
    caller.done('after ' + ms);
  }, ms);
}

So the idea is, to yield generator functions inside the runner (a generator function passed to run()), and each function should call either caller.done or caller.fail when it finishes the asynchronous work. The caller object is returned with the yield call on the first line of the function.

The Runner Function

The most complex part is the runner function itself, which iterates through all the generator functions yielded inside it.

function run(genFn) {
  let gen = genFn();

  // Store the current generator object yielded in the runner block
  let yielded;

  // The caller object, each generator should call either done or fail
  // once done with asynchronous work.
  let result = {
    done: (data) => {
      yielded = gen.next(data);
      resume();
    },
    fail: (error) => {
      yielded = gen.throw(error);
      resume();
    },
  };

  // Resume to the next generator object in the runner block
  function resume() {
    if (yielded.done) return;
    // yielded.value is the current generator object
    let genFromYield = yielded.value;

    // This will start the generator
    let r = genFromYield.next();
    if (!r.done) {
      // This will stop at the `const caller = yield;` of the generator
      r = genFromYield.next(result);
    }
  }

  // Now, start with the first generator
  yielded = gen.next();
  resume();
}

Essentially, the runner resumes whenever a generator function calls result.done or result.fail, and passes the return value from each generator function via gen.next or gen.throw in the case of errors.

Now you can write generator functions like *timeout() above to wrap your asynchronous logic. Then, inside a runner block, put them like normal synchronous function calls.

Full code