Josh Goldberg

Hacking TypeScript's async/await __awaiter for jQuery 2's "Promises"

Jul 2, 201715 minute read

Manipulating the details of TypeScript's emitted code to wrestle jQuery 2's older asynchronous constructs to work with modern `async`/`await`.

jQuery 2’s Promises are like the crusty old grandparent your parents are scared to let near the kids for fear of them saying something bizarre. Sure, they might have have been great (even trailblazers!) in their day, but times have changed and they can’t keep up.

(by “kids” I mean any reasonable modern JavaScript framework)

Problem: jQuery 2’s “Promises” aren’t A+ compliant (jQuery 3’s are). My team at work is upgrading from jQuery 2 to jQuery 3 and it’s taking a while. We’re stuck with 2’s for now. At the same time, we’d really like to use new Promise-based developments in JavaScript such as async/await.

Blessing: we’re on a transpiler (TypeScript) that converts shiny new features into plain old JavaScript. Let’s take a look at how it does that with async/await and how we can hack it to work for us.

Spoiler: see the resultant repository on GitHub…

(Re-)Intro to async/await

Take a look at this quick code sample on typescriptlang.org/playground:

const test = async () => await Promise.resolve();

That’s a lot of outputted code! Before we dive headfirst into it, we should refresh how async/await works in the first place.

At its core, marking a function as “asynchronous” indicates that the function is a generator (a function that gets stopped and started continuously, with return values in-between) that happens to mostly return Promises. Generators themselves are conceptually straightforward:

function* getSomeValues() {
	yield "foo";
	yield "bar";
}

// Logs "foo", then "bar"
for (const value of getSomeValues()) {
	console.log(value);
}

Most readers see getSomeValues() as returning "foo" then returning "bar". Astute generator-savvy nerds will know it’s really a state machine that goes to a return "foo"; when iteration=0 and return "bar"; when iteration=1.

You can think of that code as working similarly to:

function getSomeValues() {
	let timeCalled = -1;
	function next() {
		timeCalled += 1;
		switch (timeCalled) {
			case 0:
				return {
					value: "foo",
				};
			case 1:
				return {
					value: "bar",
				};
			default:
				return {
					done: true,
				};
		}
	}
	return { next };
}
// Logs "foo", then "bar"
const iterator = getSomeValues();
while (true) {
	const { done, value } = iterator.next();
	if (done) {
		break;
	}
	console.log(value);
}

async/await builds on top of this generator fanciness by providing a specialized syntax for generators that yield Promises. “awaiting” an “asynchronous” generator is shorthand for saying that you want to capture the result of that Promise locally, .then continue with the function.

See this article on the asynchronous generators in JavaScript. This article on how async/await is implemented in Python 3.5 is a pretty great if you’re still curious.

Anyway, back to the TypeScript output, there are three sections to explore:

  1. The awaiter
  2. The generator
  3. The original code

1. The __awaiter

This section of code takes a Promise-generating async function and spits out a Promise for its eventual result. The arguments given to it are important:

function (thisArg, _arguments, P, generator) {
	// ...
}

The function passed to P takes in resolve, reject and creates three functions before running generator:

Expanding step out and prettifying it a bit:

function step(result) {
	if (result.done) {
		resolve(result.value);
		return;
	}
	return new Promise(function (resolve) {
		resolve(result.value);
	}).then(fulfilled, rejected);
}

That’s the good stuff.

resolve(result.value) is a pretty lovely piece of code. result.value is what we’re given each time the asynchronous generator code (our async function) yields. It appears both when the function is .done and when it’s still going.

2. The __generator

tl;dr

3. The original code

…in __awaiter-ified form. The a.label it switches on is which iteration of the state machine / generator loop is being run. The [number, any] has a conveniently /* commented */ label for what operation is happening at that iteration loop, followed by the operation taking place.

Taking a small example function:

async function example() {
	// result.value will be promise
	const promise = Promise.resolve("foo");
	await promise;
	// result.value will be "foo"
	return "foo";
}

Here’s how it looks transformed on the playground:

function example() {
	return __awaiter(this, void 0, void 0, function () {
		var promise;
		return __generator(this, function (_a) {
			switch (_a.label) {
				case 0:
					promise = Promise.resolve("foo");
					return [4 /*yield*/, promise];
				case 1:
					_a.sent();
					// result.value will be "foo"
					return [2 /*return*/, "foo"];
			}
		});
	});
}

At this point, you might have a vague understanding of how the source code is transformed from an async function to a generator.

(Curious about the missing comment? See TypeScript issue #15323.)

Using jQuery 2’s Promises instead of Promise

Our first task for bending this mess to our will should be to get it to stop assuming a global Promise and start using jQuery 2’s JQueryPromise instead.

TypeScript’s __awaiter starts with logic to not override any existing __awaiter: var **awaiter = (this && this.**awaiter) || .... If we define our own, it’ll override all of TypeScript’s.

// No existence check here!
var __awaiter = function (/* ... */) {
	// ...
};

Instead of P and new (P || (P = Promise)), let’s define a jQueryPromiseFactory and use it in our own custom __awaiter.

function jQueryPromiseFactory(callback) {
	var deferred = $.Deferred();
	var promise = deferred.promise();
	try {
		callback(deferred.resolve, deferred.reject);
	} catch (error) {
		deferred.reject(error);
	}
	return promise;
}

Using it instead of the passed P in __awaiter for the returned P…

var __awaiter = function (thisArg, _arguments, _ignore, generator) {
    return jQueryPromiseFactory(...);
};

…and the step P later on…

function step(result) {
	if (result.done) {
		resolve(result.value);
		return;
	}
	return jQueryPromiseFactory(function (resolve) {
		resolve(result.value);
	}).then(fulfilled, rejected);
}

Very cool. We’ve now tricked TypeScript into using $.Deferred().promise() instead of the global Promise.

Retrieving Awaited Values

Right now, instead of getting back the awaited value when we await, we get Object (state, always, ...). That Object is the JQueryPromise being yielded from the generator instead of the actual value. We’ll have to instead return its resolved value.

For readability, here’s a cleaned up version of __awaiter’s body:

return jQueryPromiseFactory(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)
			: jQueryPromiseFactory(function (resolve) {
					resolve(result.value);
			  }).then(fulfilled, rejected);
	}
	step((generator = generator.apply(thisArg, _arguments || [])).next());
});

result is what’s given to us by the generator. result.done is whether the generator has completed (we should resolve as finished) and result.value is the raw value of what was returned to us (above, first promise then "bar").

Since we know that result.value will sometimes be a JQueryPromise now, and step is the thing that asynchronously waits for the next iteration, this is where we should insert our logic to wait. Changing it to respect our ways:

function step(result) {
	// Part 1
	if (!result.value || !result.value.then) {
		resolve(result.value);
		return;
	}
	// Part 2
	result.value
		.then(function (resolvedValue) {
			fulfilled(resolvedValue);
		})
		.fail(function (rejectedError) {
			rejected(rejectedError);
		});
}

Part 1 checks for the case of the function being done, but does so by seeing if the result is a .then-able.

Part 2 now assumes that the result is a JQueryPromise, and uses the jQuery .then and .fail methods to call fulfilled or rejected as necessary. Note that jQuery 2 doesn’t provide a .catch method.

return jQueryPromiseFactory(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) {
		// Part 1
		if (!result.value || !result.value.then) {
			resolve(result.value);
			return;
		}
		// Part 2
		result.value
			.then(function (resolvedValue) {
				fulfilled(resolvedValue);
			})
			.fail(function (rejectedError) {
				rejected(rejectedError);
			});
	}
	step((generator = generator.apply(thisArg, _arguments || [])).next());
});

Caveats

You should move to jQuery 3. It has standards-compliant Promises. This library should only be used as a polyfill while your team works on the update.

jQuery 2’s .then is synchronous by default but can be asynchronous. Standards-compliant Promise implementations are always asynchronous. Don’t structure your code assuming .then callbacks are run synchronously.

Because .then is synchronous, if an error is thrown synchronously in a JQueryPromise, any subsequent code (including awaits) will not be run. That means you can’t use await within try blocks on this adapter. Put the risky logic in a non-async function instead.

In Conclusion

Use this at your risk. I honestly don’t know what kind of horrible things will happen when you try this out. We’re just using it in test code for now because we’re deathly terrified of never-before-seen bugs popping up in production.

If you’re really curious about how TypeScript does this stuff, their generators.ts source code is a great thing to dive into.