Transforming Async, Await JS and minifying using Uglify

Async and Await makes asynchronous code so much cleaner and nicer to work with. It is widely supported by all the latest versions of modern browsers, including Node v7.6+.

See can i use for browser support and node.green for node support.

So let's try and minify it! Oh...

Whilst trying to use uglify-js@3 to minify some code I had that used Async and Await, I got an error like this.

Parse error at src/alpha.js:1,6
async function doAsyncTask() {
      ^
ERROR: Unexpected token: keyword (function)

So I thought "Easy, I'll just use the new uglify-es or babel-preset-minify!" which both support ES2015+. This minified code worked everywhere but on Safari in which I encountered this obscure bug when running the code in the browser (this is now fixed in the technology preview version of Safari).

The error in Safari I got looked like this:

SyntaxError: Cannot declare a let variable twice: 'i'.

I could not avoid this bug, I tried a few things but nothing seeemed to work.

So to workaround this bug I decided to compile the code using Babel and then use the stable uglify-js@3 to minify it. Thus avoiding the Safari bug (until the next version is released) as Babel also converted the let's and const's to var's.

This worked but I went through a few different Async Babel transforms first. Below are the ones I tried, I ended up using transform-async-to-generator as it was the one that the Babel documentation recommended but fast-async is also a great alternative.

Update: There is a config option to workaround this bug for UglifyJS2 from this comment.

Add this to the UglifyJS2 config:

{
    mangle: {
        safari10: true,
    }
}

or this to command line:

--mangle safari10=true

Transforming Async / Await using Babel

Example code we will transform:

async function doAsyncTask() {
  try {
    const response = await fetch('https://www.reddit.com/r/Overwatch.json');
    return response.json();
  } catch (e) {
    console.error(e);
  }
}

doAsyncTask()
  .then(data => console.log(data))
  .catch(e => console.error(e));

Using transform-async-to-generator

I'm using babel-preset-env as it contains transform-async-to-generator which will transform any Async functions to generators (depending on what browsers you want to support).

1. Add babel-preset-env

yarn add --dev babel-preset-env

2. Add .babelrc file

{
  "presets": ["env"]
}

Now once you run Babel the code it will look like this:

var doAsyncTask = function () {
  var _ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee() {
    var response;
    return regeneratorRuntime.wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
            _context.next = 2;
            return fetch('https://www.reddit.com/r/Overwatch.json');

          case 2:
            response = _context.sent;
            return _context.abrupt('return', response.json());

          case 4:
          case 'end':
            return _context.stop();
        }
      }
    }, _callee, this);
  }));

  return function doAsyncTask() {
    return _ref.apply(this, arguments);
  };
}();

function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }

doAsyncTask().then(function (data) {
  return console.log(data);
}).catch(function (e) {
  return console.error(e);
});

The async function was transformed into generator style function. But upon using this code you will get an error like this:

ReferenceError: regeneratorRuntime is not defined

If you take a look at line #2 you'll notice a reference to the function regeneratorRuntime which does not exist.

You'll need to add a regenerator runtime function here, afterwards the error will go away.

3. Adding Facebook's regenerator-runtime

  1. Add regenerator-runtime
yarn add regenerator-runtime
  1. Add the import at the top of the code
import 'regenerator-runtime/runtime';
// ...

Run it now and it should work! It should also happily minify with uglify-js@3.

Using fast-async

This excellent alternative transforms Async functions using a mixture of Promises and Generators by default. It's also apparantly faster than the Babel's default implementation.

There are downsides with fast-async though... "await anywhere, async return and async throw are not supported, however full implementation of async function containing await expressions is implemented.".

So as long as you are only doing async function and awaits inside those functions it will work for you.

1. Add fast-async

yarn add --dev fast-async

2. Add nodent-runtime

To avoid the runtime being added for every file that contains an Async function we can manually import it once.

yarn add nodent-runtime

Import it once at the top of the code:

import 'nodent-runtime'
// ...

3. Update .babelrc file

We remove the default Async transform and replace it with fast-async.

{
  "presets": [
    ["env", {
      "exclude": ["transform-regenerator"]
    }]
  ],
  "plugins": [
    ["fast-async", {
      "useRuntimeModule": true
    }]
  ]
}

Using transform-async-to-module-method

I've added this transform with bluebird for completion but I did not like this method. I had issues using this transform with rollup.js as it could not resolve bluebird's exports, Webpack may have faired better but I didn't try it. The only method needed from bluebird by this transform method is coroutine but for me using rollup.js the whole library was included.

1. Add babel-plugin-transform-async-to-module-method

yarn add --dev babel-plugin-transform-async-to-module-method

2. Add bluebird

yarn add bluebird

3. Update .babelrc file

We remove the default Async transform and replace it with transform-async-to-module-method.

{
  "presets": [
    ["env", {
      "exclude": ["transform-regenerator"]
    }]
  ],
  "plugins": [
    ["transform-async-to-module-method", {
      "module": "bluebird",
      "method": "coroutine"
    }]
  ]
}

Conclusion

I recommend either using transform-async-to-generator or fast-async if you need to transform Async functions.

This post will no doubt become deprecated within a year or so as support for Async and Await is already globally at 67.24% at time of writing. So they'll be less and less need to use any transformations at all. If you use babel-preset-env with something like "browsers": ["last 2 versions"] then one day your Async code will no longer be transformed as it will widely supported :).

I've not really touched on how awesome Async and Await is for the future of JS, many other people have wrote about that. I only hope that anyone who has had issues like me can find a solution here that fits their needs.

Thanks for reading!