Angular 1.x - Transparent Async / Await with Babel 7
Tags: javascript, angular, babel, async
Published: 2021-01-16 18:01:02

Let's say you're working on an old AngularJS 1.x codebase and you want to use async / await. These keywords don't play nice with AngularJS. Once these promises are fulfilled, you have to wrap them in `$q` (or run `$apply()`, etc.) in order to loop them into angular's digest cycle. This sucks!

We can use babel to transpile async / await and do the $q-wrapping for us. 

The only caveat is that we can only call `await` after angular bootstrap. You can still use `Promise.then`.

Here's how to do this:

1. Create a file called `ag-await.js`:

```javascript
/* eslint-disable no-undef */

// Adapted from: https://labs.magnet.me/nerds/2015/11/16/async-await-in-angularjs.html
// Babel will convert "await" to a generator, and then wrap the generator with this function
// This only works if we use await after the app has been bootstrapped
//
// This is intentionally ES5, because it's not clear if this will go through babel since it's
// a babel plugin

export function $await(generator) {
    function getInjector() {
        try {
            var angular = window.angular;
            if (!angular) throw new Error('Angular is not available on window!');
            var $injector = angular.element(document).injector();
            if (!$injector) throw new Error('Could not get angular injector for document.');
            return $injector;
        } catch (error) {
            console.error(
                [
                    'This error is probably because you have an async / await call before angular bootstrap.',
                    'Use the stack trace to find out where the issue is, and re-organize your code such that the',
                    'await is done after angular bootstrap. Alternatively, exclude the file from the babel config.',
                ].join('\n'),
            );
            throw error;
        }
    }

    return function () {
        var args = arguments;
        var self = this;
        var $injector = getInjector();
        var $q = $injector.get('$q');
        var $rootScope = $injector.get('$rootScope');

        var promise = (function () {
            var deferred = $q.defer();
            var iter;

            try {
                iter = generator.apply(self, args);
            } catch (e) {
                deferred.reject(e);
                return;
            }

            function next(val, isError = false) {
                var state;

                try {
                    state = isError ? iter.throw(val) : iter.next(val);
                } catch (e) {
                    deferred.reject(e);
                    return;
                }

                if (state.done) {
                    deferred.resolve(state.value);
                } else {
                    $q.when(state.value).then(next, function (err) {
                        next(err, true);
                    });
                }
            }
            next();

            return deferred.promise;
        })();

        return promise['finally'](function () {
            try {
                $rootScope.$evalAsync();
                // eslint-disable-next-line no-empty
            } catch (error) {}
        });
    };
}
```

Then in your babel config:

```javascript
{
    plugins: [
        [
            '@babel/plugin-transform-async-to-generator',
            {
                module: require.resolve('./ag-await.js'),
                method: '$await',
            },
        ],
        '@babel/plugin-transform-regenerator'
    ],
    presets: [
        [
            '@babel/preset-env',
            {
                useBuiltIns: 'usage',
                corejs: 3,
                targets: '> 0.25%, not dead',
                bugfixes: true,
                exclude: [
                    '@babel/plugin-transform-regenerator',
                    '@babel/plugin-proposal-async-generator-functions',
                    '@babel/plugin-transform-async-to-generator',
                ],
            },
        ]
    ],
}
```

You can now do this:
```javascript
app.controller("Foo", ($scope) => {
    const response = await fetch('https://example.com/data.json');
    $scope.data = await response.json();
})
```