Generators + Promises

It is possible to express a series of promises in a chain to represent the async flow control of your program. Consider:

  1. step1()
  2. .then(
  3. step2,
  4. step1Failed
  5. )
  6. .then(
  7. function step3(msg) {
  8. return Promise.all( [
  9. step3a( msg ),
  10. step3b( msg ),
  11. step3c( msg )
  12. ] )
  13. }
  14. )
  15. .then(step4);

However, there’s a much better option for expressing async flow control, and it will probably be much more preferable in terms of coding style than long promise chains. We can use what we learned in Chapter 3 about generators to express our async flow control.

The important pattern to recognize: a generator can yield a promise, and that promise can then be wired to resume the generator with its fulfillment value.

Consider the previous snippet’s async flow control expressed with a generator:

  1. function *main() {
  2. try {
  3. var ret = yield step1();
  4. }
  5. catch (err) {
  6. ret = yield step1Failed( err );
  7. }
  8. ret = yield step2( ret );
  9. // step 3
  10. ret = yield Promise.all( [
  11. step3a( ret ),
  12. step3b( ret ),
  13. step3c( ret )
  14. ] );
  15. yield step4( ret );
  16. }

On the surface, this snippet may seem more verbose than the promise chain equivalent in the earlier snippet. However, it offers a much more attractive — and more importantly, a more understandable and reason-able — synchronous-looking coding style (with = assignment of “return” values, etc.) That’s especially true in that try..catch error handling can be used across those hidden async boundaries.

Why are we using Promises with the generator? It’s certainly possible to do async generator coding without Promises.

Promises are a trustable system that uninverts the inversion of control of normal callbacks or thunks (see the Async & Performance title of this series). So, combining the trustability of Promises and the synchronicity of code in generators effectively addresses all the major deficiencies of callbacks. Also, utilities like Promise.all([ .. ]) are a nice, clean way to express concurrency at a generator’s single yield step.

So how does this magic work? We’re going to need a runner that can run our generator, receive a yielded promise, and wire it up to resume the generator with either the fulfillment success value, or throw an error into the generator with the rejection reason.

Many async-capable utilities/libraries have such a “runner”; for example, Q.spawn(..) and my asynquence’s runner(..) plug-in. But here’s a stand-alone runner to illustrate how the process works:

  1. function run(gen) {
  2. var args = [].slice.call( arguments, 1), it;
  3. it = gen.apply( this, args );
  4. return Promise.resolve()
  5. .then( function handleNext(value){
  6. var next = it.next( value );
  7. return (function handleResult(next){
  8. if (next.done) {
  9. return next.value;
  10. }
  11. else {
  12. return Promise.resolve( next.value )
  13. .then(
  14. handleNext,
  15. function handleErr(err) {
  16. return Promise.resolve(
  17. it.throw( err )
  18. )
  19. .then( handleResult );
  20. }
  21. );
  22. }
  23. })( next );
  24. } );
  25. }

Note: For a more prolifically commented version of this utility, see the Async & Performance title of this series. Also, the run utilities provided with various async libraries are often more powerful/capable than what we’ve shown here. For example, asynquence’s runner(..) can handle yielded promises, sequences, thunks, and immediate (non-promise) values, giving you ultimate flexibility.

So now running *main() as listed in the earlier snippet is as easy as:

  1. run( main )
  2. .then(
  3. function fulfilled(){
  4. // `*main()` completed successfully
  5. },
  6. function rejected(reason){
  7. // Oops, something went wrong
  8. }
  9. );

Essentially, anywhere that you have more than two asynchronous steps of flow control logic in your program, you can and should use a promise-yielding generator driven by a run utility to express the flow control in a synchronous fashion. This will make for much easier to understand and maintain code.

This yield-a-promise-resume-the-generator pattern is going to be so common and so powerful, the next version of JavaScript after ES6 is almost certainly going to introduce a new function type that will do it automatically without needing the run utility. We’ll cover async functions (as they’re expected to be called) in Chapter 8.