How is async/await transpiled to ES5

Category:

In short, ES7's async/await syntax is used to turn asynchronous code flows with Promises into synchronous ones. The proposal and examples can be seen here.

We will look at the implementation details of ES7's async/await to get a better understanding of it. You can use babel-preset-stage-3 with its transform-async-generator-functions plugin to transpile the async/await syntax ES6's generator functions. Then the generators are again transformed to ES5 functions with state, the process is described in this blog post. All there is left to understand is how to reduce async functions to generators.

#async/await to generators

Suppose we have an async function - let's take the example from the proposal:

async function chainAnimationsAsync(elem, animations) {
    let ret = null;
    try {
        for(const anim of animations) {
            ret = await anim(elem);
        }
    } catch(e) { /* ignore and keep going */ }
    return ret;
}

Rewriting it with generators requires the following high-level ideas:

  1. Turn the async function into a generator function and make it return a Promise
  2. Replace each await in the function's code with a yield

We accomplish the first goal by creating an iterator for the generator function and iterating it until the function is finished or an error occurred. We wrap this behavior into a function called spawn.

function chainAnimationsGenerator(elem, animations) {
    // spawn will return a Promise
    return spawn(function*() {
        let ret = null;
        try {
            for(const anim of animations) {
                ret = yield anim(elem);
            }
        } catch(e) { /* ignore and keep going */ }
        return ret;
    });
}

In essence, the spawn function wraps the result (coming from the generator's yield) into a promise and keeps iterating the generator in the promise's resolve callback (while passing the resolved value as the new argument to yield). This chaining of the code execution through promises is what makes it possible to execute asynchronous code as it was synchronous code:

function spawn(genFunc, self) {
    return new Promise(function(resolve, reject) {
        // start iterating the original function and set correct this pointer
        var iterator = genFunc.call(self);  
        function step(nextFunc) {
            var next;
            try {
                next = nextFunc();
            } catch(e) {
                // finished with failure, reject the promise
                reject(e);
                return;
            }
            if(next.done) {
                // finished with success, resolve the promise
                resolve(next.value);
                return;
            }
            // not finished, chain off the yielded promise and `step` again
            Promise.resolve(next.value).then(function(v) {
                // keep stepping until next yield (original await) passing new value to yield
                step(function() { return iterator.next(v); });
            }, function(e) {
                step(function() { return iterator.throw(e); });
            });
        }
        // keep stepping until next yield (original await)
        step(function() { return iterator.next(); });
    });
}

Update: I highly recommend reading Ben Nadel's article for a more in depth explanation.