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 await
s 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