jQuery 2’s Promises are like the crusty old grandparent your parents are scared to let near the kids. 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 Promise s. Generators themselves are conceptually straightforward:

function* getSomeValues() {

yield "foo";

yield "bar";

} // Logs "foo", then "bar"

for (const value of getSomeValues()) {

console.log(value);

}

Normal 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 Promise s. “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 subject in JavaScript. This article on how it’s 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:

The __awaiter The __generator 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) {

thisArg is the parent scope. Uninteresting.

_arguments will be passed to the await ed function. Uninteresting.

will be passed to the ed function. Uninteresting. P is the Promise class we’ll be using. We can see later on that it defaults to the global Promise via (P || P = Promise) .

is the class we’ll be using. We can see later on that it defaults to the global via . generator is the async hronous generator (technically an Iterable ) that is trying to await something. Presumably it should yield Promise s.

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

fulfilled is run each time generator resolves a value . It tries to run again using step(generator.next(value)) inside a try/catch , and if that fails calls reject . Moderately uninteresting.

is run each time resolves a . It tries to run again using inside a , and if that fails calls . Moderately uninteresting. rejected is run each time generator gives a rejection value . It tries to yield the generator’s throw logic using step(generator["throw"](value)) inside a try/catch , and if that fails calls reject . Moderately uninteresting.

is run each time gives a rejection . It tries to yield the generator’s throw logic using inside a , and if that fails calls . Moderately uninteresting. step is where the magic happens. It attempts to get the next value from generator using a new P Promise and the previous two functions until the result it’s given is .done , at which point it calls resolve .

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 sexy 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 simple 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 should 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 our sanity, 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 await s) 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.

github.com/joshuakgoldberg/jquery-2-typescript-async-await-adapter

npm install jquery-2-typescript-async-await-adapter

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