05
Async functions: a generator driven by a promise trampoline
A generator can pause at
yield and be resumed with a value through next(value), or with an error through throw(error). Yield promises, and let a small loop resume the generator whenever each one settles: you get asynchronous code that reads top to bottom. async/await is that loop built into the language.The key insight is a single, concise method that decorates a generator, creating an internal “promise trampoline”.
Generator + promise trampoline
yield1var authenticate = Promise.async(function *() {2 var username = yield getUsernameFromConsole();3 var user = getUserFromDatabase(username);4 var password = getPasswordFromConsole();5 [user, password] = yield Promise.all([user, password]);6 if (hash(password) !== user.passwordHash) {7 throw new Error("password is invalid");8 }9 return user.name;10});
The same function with async / await
await1var authenticate = async function () {2 var username = await getUsernameFromConsole();3 var user = getUserFromDatabase(username);4 var password = getPasswordFromConsole();5 [user, password] = await Promise.all([user, password]);6 if (hash(password) !== user.passwordHash) {7 throw new Error("password is invalid");8 }9 return user.name;10};
Trampoline
idleCall authenticate(). Each resume of the generator is one step.
Pending promises
Nothing in flight.
The outer promise
authenticate()not called
Returned synchronously the moment you call the function. The trampoline forwards the generator's return to its resolver, and any uncaught throw to its rejection.
Mark Miller's async decorator
Promise.async = function async(generate) {
return function () {
function resume(verb, argument) {
var result;
try {
result = generator[verb](argument);
} catch (exception) {
return Promise.throw(exception);
}
if (result.done) {
return result.value;
} else {
return Promise.return(result.value).then(donext, dothrow);
}
}
var generator = generate.apply(this, arguments);
var donext = resume.bind(this, "next");
var dothrow = resume.bind(this, "throw");
return donext();
};
};Why special syntax anyway?
await binds tighter than yield, so it composes inside expressions. Decoupling async functions from generator functions also frees yield for something else: async generator functions, the plural and temporal getter.
async function addPromises(a, b) {
return await a + await b;
}- why it mattersThe trampoline has three outcomes per step: yield waits on the promise and resumes, return fulfills the outer promise, and throw rejects it.
- why it mattersA rejected yielded promise doesn't crash the trampoline: it is thrown into the generator at the paused
yield, where an ordinarytry/catchcould recover. - why it mattersStarting the database and password lookups before yielding on
Promise.allruns them concurrently. Watch both bars fill at once.