05

Async functions: a generator driven by a promise trampoline

A generator can pause at yield and be resumed with a value through next(value), or with an error through throw(error). Yield promises, and let a small loop resume the generator whenever each one settles: you get asynchronous code that reads top to bottom. async/await is that loop built into the language.
The key insight is a single, concise method that decorates a generator, creating an internal “promise trampoline”.

Generator + promise trampoline

yield
1var authenticate = Promise.async(function *() {
2 var username = yield getUsernameFromConsole();
3 var user = getUserFromDatabase(username);
4 var password = getPasswordFromConsole();
5 [user, password] = yield Promise.all([user, password]);
6 if (hash(password) !== user.passwordHash) {
7 throw new Error("password is invalid");
8 }
9 return user.name;
10});

The same function with async / await

await
1var authenticate = async function () {
2 var username = await getUsernameFromConsole();
3 var user = getUserFromDatabase(username);
4 var password = getPasswordFromConsole();
5 [user, password] = await Promise.all([user, password]);
6 if (hash(password) !== user.passwordHash) {
7 throw new Error("password is invalid");
8 }
9 return user.name;
10};

Trampoline

idle
Call authenticate(). Each resume of the generator is one step.

Pending promises

Nothing in flight.

The outer promise

authenticate()not called

Returned synchronously the moment you call the function. The trampoline forwards the generator's return to its resolver, and any uncaught throw to its rejection.

Mark Miller's async decorator

Promise.async = function async(generate) {
  return function () {
    function resume(verb, argument) {
      var result;
      try {
        result = generator[verb](argument);
      } catch (exception) {
        return Promise.throw(exception);
      }
      if (result.done) {
        return result.value;
      } else {
        return Promise.return(result.value).then(donext, dothrow);
      }
    }
    var generator = generate.apply(this, arguments);
    var donext = resume.bind(this, "next");
    var dothrow = resume.bind(this, "throw");
    return donext();
  };
};

Why special syntax anyway?

await binds tighter than yield, so it composes inside expressions. Decoupling async functions from generator functions also frees yield for something else: async generator functions, the plural and temporal getter.

async function addPromises(a, b) {
  return await a + await b;
}
  • why it mattersThe trampoline has three outcomes per step: yield waits on the promise and resumes, return fulfills the outer promise, and throw rejects it.
  • why it mattersA rejected yielded promise doesn't crash the trampoline: it is thrown into the generator at the paused yield, where an ordinary try/catch could recover.
  • why it mattersStarting the database and password lookups before yielding on Promise.all runs them concurrently. Watch both bars fill at once.