An interesting tactic when researching language features is to re-create newer features, using prior, earlier supported features. For example, is it possible to approximate the features and syntax flow provided by async/await, but using only generators and Promises?


Baseline example in native async/await

The canonical example of the usefulness of async/await is needing to do something with the result of an http request. In this case we are making several requests using request-promise-native. We could just as well promisify http.get() but that involves a bit more code, and might muddy the waters of providing a more simple example.

const rp = require('request-promise-native');

async function getPostsNativeAsync() {
  const response1 = await rp('http://jsonplaceholder.typicode.com/posts/1');
  console.log(response1);
  const response2 = await rp('http://jsonplaceholder.typicode.com/posts/2');
  console.log(response2);
  const response3 = await rp('http://jsonplaceholder.typicode.com/posts/3');
  console.log(response3);
}

getPostsNativeAsync();

Using a generator + Promise wrapper

The following example does not demonstrate a polyfill (the async and await keywords are not filled). Nor is it a transpilation example. However, the awaitWithGenerator() wrapper function provides similar functionality and syntactical brevity for our asynchronous code. What follows is an approximate recreation of the above async/await example using the wrapper function. The source of the wrapper function itself appears later.

const rp = require('request-promise-native');

const awaitWithGenerator = require('./awaitWithGenerator');

function getPostsWithGenerator() {
  awaitWithGenerator(function* () {
    const response1 = yield rp('http://jsonplaceholder.typicode.com/posts/1');
    console.log(response1);
    const response2 = yield rp('http://jsonplaceholder.typicode.com/posts/2');
    console.log(response2);
    const response3 = yield rp('http://jsonplaceholder.typicode.com/posts/3');
    console.log(response3);
  });
}

getPostsWithGenerator();

The source of the wrapper

After digging in a little further on generators, I arrived at the following wrapper function, which I used in the prior example. As an aside, one very valuable takeaway from thinking about generator functions in general is that is that they are not in and of themselves asynchronous. But, because generator functions allow you to enter and exit their body, multiple times if needed, all the while preserving their context and variables state, they can be combined with Promises to create powerful async constructs. Take a look at the following wrapper function and the preceding example that uses it. See if you can step through when we enter and exit the generator body. Likewise, notice when we pass off the resolved value from the wrapped Promise in the helper, to the various yielded variables in the body of the generator.

module.exports = function awaitWithGenerator(generator) {
  return new Promise(resolve => {
    const gen = generator(); // Initialize the generator.

    const doNext = result => {
      if (result.done) {
        return resolve(result.value); // Iterator is finished, resolve final yield value.
      } else {
        return new Promise(resolve => resolve(result.value)) // Otherwise, resolve the yielded Promise.
          .then(value => doNext(gen.next(value))); // Pass the resolved value back to generator and continue.
      }
    };

    return doNext(gen.next()); // Start generator with initial step through yield value(s).
  });
};

Showing how to approximate newer features with older technology is a helpful pursuit. Hopefully this succinct illustration of async/await capabilities using generators and Promises has proven the value of that approach. It's interesting when we start to think of asynchronous code as less about controlling time, and really more as controlling when we enter and exit a context, representing the eventual values of variables, within a construct that allows us to preserve state within a context, as generators do.