Created
December 6, 2015 17:53
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"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