Iterating Generators Asynchronously

What do generators have to do with async coding patterns, fixing problems with callbacks, and the like? Let’s get to answering that important question.

We should revisit one of our scenarios from Chapter 3. Let’s recall the callback approach:

  1. function foo(x,y,cb) {
  2. ajax(
  3. "http://some.url.1/?x=" + x + "&y=" + y,
  4. cb
  5. );
  6. }
  7. foo( 11, 31, function(err,text) {
  8. if (err) {
  9. console.error( err );
  10. }
  11. else {
  12. console.log( text );
  13. }
  14. } );

If we wanted to express this same task flow control with a generator, we could do:

  1. function foo(x,y) {
  2. ajax(
  3. "http://some.url.1/?x=" + x + "&y=" + y,
  4. function(err,data){
  5. if (err) {
  6. // throw an error into `*main()`
  7. it.throw( err );
  8. }
  9. else {
  10. // resume `*main()` with received `data`
  11. it.next( data );
  12. }
  13. }
  14. );
  15. }
  16. function *main() {
  17. try {
  18. var text = yield foo( 11, 31 );
  19. console.log( text );
  20. }
  21. catch (err) {
  22. console.error( err );
  23. }
  24. }
  25. var it = main();
  26. // start it all up!
  27. it.next();

At first glance, this snippet is longer, and perhaps a little more complex looking, than the callback snippet before it. But don’t let that impression get you off track. The generator snippet is actually much better! But there’s a lot going on for us to explain.

First, let’s look at this part of the code, which is the most important:

  1. var text = yield foo( 11, 31 );
  2. console.log( text );

Think about how that code works for a moment. We’re calling a normal function foo(..) and we’re apparently able to get back the text from the Ajax call, even though it’s asynchronous.

How is that possible? If you recall the beginning of Chapter 1, we had almost identical code:

  1. var data = ajax( "..url 1.." );
  2. console.log( data );

And that code didn’t work! Can you spot the difference? It’s the yield used in a generator.

That’s the magic! That’s what allows us to have what appears to be blocking, synchronous code, but it doesn’t actually block the whole program; it only pauses/blocks the code in the generator itself.

In yield foo(11,31), first the foo(11,31) call is made, which returns nothing (aka undefined), so we’re making a call to request data, but we’re actually then doing yield undefined. That’s OK, because the code is not currently relying on a yielded value to do anything interesting. We’ll revisit this point later in the chapter.

We’re not using yield in a message passing sense here, only in a flow control sense to pause/block. Actually, it will have message passing, but only in one direction, after the generator is resumed.

So, the generator pauses at the yield, essentially asking the question, “what value should I return to assign to the variable text?” Who’s going to answer that question?

Look at foo(..). If the Ajax request is successful, we call:

  1. it.next( data );

That’s resuming the generator with the response data, which means that our paused yield expression receives that value directly, and then as it restarts the generator code, that value gets assigned to the local variable text.

Pretty cool, huh?

Take a step back and consider the implications. We have totally synchronous-looking code inside the generator (other than the yield keyword itself), but hidden behind the scenes, inside of foo(..), the operations can complete asynchronously.

That’s huge! That’s a nearly perfect solution to our previously stated problem with callbacks not being able to express asynchrony in a sequential, synchronous fashion that our brains can relate to.

In essence, we are abstracting the asynchrony away as an implementation detail, so that we can reason synchronously/sequentially about our flow control: “Make an Ajax request, and when it finishes print out the response.” And of course, we just expressed two steps in the flow control, but this same capability extends without bounds, to let us express however many steps we need to.

Tip: This is such an important realization, just go back and read the last three paragraphs again to let it sink in!

Synchronous Error Handling

But the preceding generator code has even more goodness to yield to us. Let’s turn our attention to the try..catch inside the generator:

  1. try {
  2. var text = yield foo( 11, 31 );
  3. console.log( text );
  4. }
  5. catch (err) {
  6. console.error( err );
  7. }

How does this work? The foo(..) call is asynchronously completing, and doesn’t try..catch fail to catch asynchronous errors, as we looked at in Chapter 3?

We already saw how the yield lets the assignment statement pause to wait for foo(..) to finish, so that the completed response can be assigned to text. The awesome part is that this yield pausing also allows the generator to catch an error. We throw that error into the generator with this part of the earlier code listing:

  1. if (err) {
  2. // throw an error into `*main()`
  3. it.throw( err );
  4. }

The yield-pause nature of generators means that not only do we get synchronous-looking return values from async function calls, but we can also synchronously catch errors from those async function calls!

So we’ve seen we can throw errors into a generator, but what about throwing errors out of a generator? Exactly as you’d expect:

  1. function *main() {
  2. var x = yield "Hello World";
  3. yield x.toLowerCase(); // cause an exception!
  4. }
  5. var it = main();
  6. it.next().value; // Hello World
  7. try {
  8. it.next( 42 );
  9. }
  10. catch (err) {
  11. console.error( err ); // TypeError
  12. }

Of course, we could have manually thrown an error with throw .. instead of causing an exception.

We can even catch the same error that we throw(..) into the generator, essentially giving the generator a chance to handle it but if it doesn’t, the iterator code must handle it:

  1. function *main() {
  2. var x = yield "Hello World";
  3. // never gets here
  4. console.log( x );
  5. }
  6. var it = main();
  7. it.next();
  8. try {
  9. // will `*main()` handle this error? we'll see!
  10. it.throw( "Oops" );
  11. }
  12. catch (err) {
  13. // nope, didn't handle it!
  14. console.error( err ); // Oops
  15. }

Synchronous-looking error handling (via try..catch) with async code is a huge win for readability and reason-ability.