async.js is a fascinating library for taming asynchronous javascript. It’s highly experimental, and as far as I know, it only works in firefox. But the idea is an important (and useful) one, so I think it’s definitely worth knowing about.

There are a few approaches to dealing with asynchronous javascript:

  1. Compile [some language] to javascript This is the approach taken by GWT, pyjamas, and many others. It’s usually extremely heavyweight, so it mainly makes sense for big apps.

  2. Compile [almost-javascript] to javascript Most notably this includes Narrative Javascript and its successor, Strands. This is a reasonable approach, but the lack of maturity / tool support makes debugging extremely hard. Also, I ran into a number of bugs in both these libraries. The thought of finding and fixing more of those bugs is not at all fun.

  3. Write a javascript library This is doable, but typically looks hideous, convoluted, and is usually quite burdensome to try and use.

Async.js is the most plausible attempt at #3 that I’ve seen. It uses a reasonably clever (but not unknown) trick to to turn Javascript 1.7’s generator functionality into an event-based coroutine system. Specifically, this allows for a program to “wait” for a callback, while not actually blocking the javascript interpreter (as a synchronous AJAX call would). Go read the async.js page to learn more, because the following isn’t going to make much sense if you don’t know roughly how to use async.js.

The divide between functions and async-aware functions

Using the default “function call descriptor” (henceforth referred to as FCD) provided by the to function, your functions are required to take precisely two arguments - a list of regular arguments, and a callback function. This is awkward, as it is not at all like typical javascript functions.

Passing in a function as an argument to the to function allows for a more natural use, where you simply provide one less than the number of arguments expected, and the FCD will fill in that last argument with the appropriate callback function. I think this use is much more natural, because it allows you to use async’s functionality to wait for functions that know nothing of async.js.

Still, I think more can be done. With the current implementation, utilizing async.js still means you have to:

  1. use the yield keyword
  2. wrap your function in a call to to
  3. provide the arguments not to your function, but to the result of the to call

The yield requirement can’t be removed without a preprocessor, but I think that for most cases, that should be all you need. Essentially, when you yield to a FCD using to(func)(arg1, arg2, arg3), you need to be calling a function that expects a callback object as its 4th (and final) argument. I think that providing one less argument than required is enough to indicate that you are trying to generate a FCD. Therefore, I have written a wrapper function (called baked) that checks the number of arguments provided against the number of arguments the function it wraps would normally expect. If in this example you were to provide all four arguments, the wrapped function would be called normally. If, however, you only provide three arguments, the wrapper will return an FCD.

That is:

yield func(a,b,c);

would yield a FCD for async.js to deal with, while:

func(a,b,c, function() { /* do whatever... */ }

would call the function normally.

This allows you to call a function directly if you are providing your own callback, and all you need to do when using async.js is to use the yield keyword, and omit the callback argument (which is easy enough, since you likely won’t have a callback function to give it).

Using it

If this is the kind of thing that interests you, please check out my js-bakery project on github (specifically, async-bakery.js). The basic usage is that every function participating in async.js behaviour (either by yielding, or by calling a function that will itself yield) must be “baked”. You can do this on a function-level by using myFunction.*bake()*, or you can create a constructor function that will automatically bake all of its methods by using MyConstructor.BakedConstructor().

It should probably be noted that this system will only work reliably for functions with fixed-length arguments. Any function that takes a variable number of arguments will still need to be wrapped in a call to to in order to force a FCD to be generated. Thankfully, functions that take variable-length arguments and a callback are few and far-between.

An example for my impatient friends…

The following example is a fairly complete depiction of how you might use baking with async.js. This assumes only that your page includes both async.js and async-bakery.js.

    Person = function(name) {
      this.name=name;

      this.get_input = function(_prompt, cb) {
        // some arbitrary async function
        window.setTimeout(function() {
          var surname = prompt(_prompt);
          console.log("returning " + surname + " into function " + cb)
          cb(surname)
        }, 100);
      }
      
    }.BakeConstructor();
    // you can Bake a constructor, which will ensure
    // that each of its instance methods are bake()'d

    // BakeConstructor() works on prototype functions, too:
    Person.prototype.get_surname = function(cb) {
      // any baked function will yield a function call descriptor
      // if you supply exactly one less argument than it expects
      this.surname = yield this.get_input("what's your surname?");
      cb();
    }

    Person.prototype.get_age = function(cb) {
      // "vanilla" callbacks are still used, if provided
      var self = this;
      this.get_input("what's your age?", function(age) {
        self.age = age;
        cb();
      });
    }

    function thatFunctionThatUsesCallbacks(callback) {
        window.setTimeout(function() {
          callback("callback result!")
        }, 1000);
    }

    var main2 = function(){
      info("main2()")
      var person = new Person("Fred");
      yield person.get_surname();
      yield person.get_age();
      console.log("person.surname = " + person.surname);
      console.log("person.age = " + person.age);

      // you can easily yield a FDC for thatFunctionThatUsesCallbacks,
      // provided you bake() it *exactly once*
      thatFunctionThatUsesCallbacks = thatFunctionThatUsesCallbacks.bake();
      console.log(yield thatFunctionThatUsesCallbacks());
      console.log("all done!");
    }.bake();