Skip to content

Instantly share code, notes, and snippets.

@dpratt
Created December 6, 2015 17:53
Show Gist options
  • Save dpratt/c3be018eb8d533d674c3 to your computer and use it in GitHub Desktop.
Save dpratt/c3be018eb8d533d674c3 to your computer and use it in GitHub Desktop.
An example of how ES6 generators can be used to make async code look synchronous
"use strict";
//A basic generator that iterates across four values.
function* simpleGenerator() {
yield 1;
yield 2;
yield 3;
return "HI THERE";
}
function testGenerator() {
//create the generator. note, at this point, none of the code
// inside simpleGenerator above has run yet - it actually starts
// execution on the first call to next()
const gen = simpleGenerator();
console.log(gen.next()); // prints '{ value: 1, done: false }'
console.log(gen.next()); // prints '{ value: 2, done: false }'
console.log(gen.next()); // prints '{ value: 3, done: false }'
console.log(gen.next()); // prints '{ value: 'HI THERE', done: true }'
console.log(gen.next()); // prints '{ value: undefined, done: true }'
}
//Convert a generator function that yields promises into a regular function
// that returns a promise.
function genToAsync(generatorFn) {
return function() {
//create the generator - this applies the original generator function
// which gives us a generator. At this point, no code in the original
// function has actually run yet - it will actually enter when we
// call generator.next() for the first time
const generator = generatorFn.apply(this, arguments);
return new Promise( (resolve, reject) => {
//Respond to a value yielded by the generator. This does one of three things:
// 1) If the generator signals that it is done, we resolve our promise
// with the yielded value and exit.
// 2) If the generator yields a value that is eventually resolved successfully,
// pass this resolved value back into the generator using next(), and then
// go back to step 1 with the value yielded by this call to next()
// 3) If the generator yields a rejected promise, re-throw this error inside the
// generator by calling generator.throw(). If the generator does not catch
// the exeception, we reject our promise with this exception and exit.
function step(generatorState) {
if(generatorState.done) {
//the async function has completed or returned a value
// resolve the promise we return to our actual caller
resolve(generatorState.value);
} else {
//the generator still has values to yield.
// wrap the yielded value in a promise and then pass it back
// to the generator
Promise.resolve(generatorState.value)
.then(resolvedValue => {
//the call to next(resolvedValue) here returns control back
// to the generator function at the point where the last 'yield'
// statement was invoked - this also sets the return value from the yield
// call to be whatever is in resolvedValue
step(generator.next(resolvedValue));
})
.catch(err => {
//This means that the generator function yielded a promise to us
// that eventually got rejected. We need to take the error wrapped in
// this promise and re-throw it back inside the generator, so it has
// a chance to handle it.
try {
//Re-throw the error yielded by the promise back inside the
// async function and give it a chance to catch it
// This has the effect of making it look like the 'yield' statement
// inside the generator threw an exception.
step(generator.throw(err));
} catch(err) {
//if we make it here, it means the generator did not catch
// the exception - we need to reject the promise that we returned to
// the caller of our function.
reject(err);
}
})
;
}
}
//start the generator and handle the first value returned from it.
step(generator.next());
});
}
}
//Mock out an async call - this immediately returns a promise of the supplied value
function immedate(value) {
return Promise.resolve(value);
}
function deferred(value) {
return new Promise(resolve => {
setTimeout(() => resolve(value), 100);
});
}
function deferredError(message) {
return Promise.reject(message);
}
const simple = genToAsync(function* () {
const first = yield deferred("This is the first part ");
//note - values yielded do not have to be promises
// it works with any value
const second = yield "of a deferred value ";
const third = yield deferred("that looks like synchronous code.");
return first + second + third;
});
const caught = genToAsync(function* () {
try {
const first = yield deferred("Before the error.");
console.log(first);
yield deferredError("Should not bubble up to the parent.");
} catch(err) {
//we should reach this point
const second = yield deferred("Caught error.");
console.log(second);
return "Successful.";
}
//should not reach here
throw new Error("Expected to catch an exception.");
});
const uncaught = genToAsync(function* () {
const first = yield deferred("Before the error.");
console.log(first);
const second = yield deferredError("This should bubble up to the parent.");
return "Should not reach here.";
});
Promise.resolve()
.then(() => simple())
.then(x => console.log(x))
.then(() => caught())
.then(x => console.log(x))
.then(() => uncaught())
.then(x => console.log(x))
.catch(err => console.log("Async function threw error: " + err))
;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment