Gowind's site

Transforming async await - I

This is the first article of a three part series: Part II, Part III

When compiling some Typescript code into JS for a backend service at work, I had set the target to es5 and I saw that the emitted code did not have any async/await statements. async/await syntax was not introduced in JS until ES2017, but clearly we are able to transpile code with async/await into es5 or es2015 JS.

So how does async/await work ? Lets transpile this to ES2015 JS and see for ourselves.

(I could have also chosen ES5, but ES5 does not have native support for Promises and implementing Promises on ES5 would have become even more complicated, so I am sticking to ES2015 (or ES6) which has native Promises, so we only have to figure out how to implement async/await )

Here is a snippet using async/await

async function getTextOrBust() {
	const resp = await fetch("https://google.com");
	if(resp.ok) {
		const body = await resp.text();
		return body;
	} else {
		throw Error("Cannot fetch goog");
	}
}
(async () => {
	let k = await getTextOrBust(4);
	console.log(k);
})();

getTextOrBust makes a https call to “google.com” and if the response is HTTP 200, returns the body (as text) of the response. Both fetch and .text() methods return a Promise, so to use them as normal values, we need to prefix them with an await keyword.

await expressions are not allowed in the code, unless they are inside functions marked async, so our getTextOrBust becomes an async function.

Since async functions cannot be used at the top level (node x.js), as top-level awaits weren’t added until ES2022, I am simulating a top-level await by creating an IIFE (immediately invoked function expression, to run the async function in the module till completion)

The Typescript Playground generated the following es6 JS for the snippet:

"use strict";
var __awaiter = (this && this.__awaiter) || function(thisArg, _arguments, P, generator) {
    function adopt(value) {
        return value instanceof P ? value : new P(function(resolve) {
            resolve(value);
        });
    }
    return new(P || (P = Promise))(function(resolve, reject) {
        function fulfilled(value) {
            try {
                step(generator.next(value));
            } catch (e) {
                reject(e);
            }
        }

        function rejected(value) {
            try {
                step(generator["throw"](value));
            } catch (e) {
                reject(e);
            }
        }

        function step(result) {
            result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
        }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};

function getANumber() {
    return __awaiter(this, void 0, void 0, function*() {
        return 4;
    });
}

function getTextOrBust() {
    return __awaiter(this, void 0, void 0, function*() {
        const resp = yield fetch("https://google.com");
        if (resp.ok) {
            const body = yield resp.text();
            return body;
        } else {
            throw Error("Cannot fetch goog");
        }
    });
}(() => __awaiter(void 0, void 0, void 0, function*() {
    let k = yield getTextOrBust();
    console.log(k);
}))();

I re-wrote the generated JS snippet a little bit to make it easier to understand

"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { 
        return value instanceof P ? value : new P(function (resolve) { resolve(value); }); 
    }
    return new (P || (P = Promise))(function (resolve, reject) {
        const genInstance = generator.apply(thisArg, _arguments || []);
        const fulfilled = (value) => { try { 
                      step(genInstance.next(value)); 
                    } catch (e) { reject(e); }
        } 
        const rejected = (value) => { try { step(genInstance["throw"](value)); } catch (e) { reject(e); } }

        function step(result) { 
            if(result.done) { 
              resolve(result.value) 
            } else {
              adopt(result.value).then(fulfilled, rejected); 
            }
        }
        step(genInstance.next());
    });
};

function getTextOrBust() {
    return __awaiter(this, void 0, void 0, function* () {
        const resp = yield fetch("https://google.com");
        if (resp.ok) {
            const body = yield resp.text();
            return body;
        }
        else {
            throw Error("Cannot fetch goog");
        }
    });
}
(() => __awaiter(void 0, void 0, void 0, function* () {
    let k = yield getTextOrBust(4);
    console.log(k);
}))();

async function x becomes __awaiter(thisArg, …, function*())

Notice how our fn getTextOrBust lost the async prefix:

async function getTextOrBust() {
	const resp = await fetch("https://google.com");
	if(resp.ok) {
		const body = await resp.text();
		return body;
	} else {
		throw Error("Cannot fetch goog");
	}
}

and became

function getTextOrBust() {
    return __awaiter(this, void 0, void 0, function* () {
        const resp = yield fetch("https://google.com");
        if (resp.ok) {
            const body = yield resp.text();
            return body;
        }
        else {
            throw Error("Cannot fetch goog");
        }
    });
}

we removed the async keyword and wrapped the body of our function in an return __awaiter(this, void 0, void 0, function* () and replaced await with yield

But, What is yield ? and what are function*, void 0, ?

In JS, void expr evaluates expr and returns undefined as the value of the expression, so let x = void 10, evaluates 10 and returns undefined as the value of x

What is function* and yield ? For that we must detour into a relatively obscure feature of Javascript : Generators

Lets take a look about Generators in Part II

Reply to this post by email ↪