ES6 In Depth: Generators, continued

ES6 In Depth is a series on new features being added to the JavaScript programming language in the 6th Edition of the ECMAScript standard, ES6 for short.

Welcome back to ES6 In Depth! I hope you had as much fun as I did during our summer break. But the life of a programmer cannot be all fireworks and lemonade. It’s time to pick up where we left off—and I’ve got the perfect topic to resume with.

Back in May, I wrote about generators, a new kind of function introduced in ES6. I called them the most magical feature in ES6. I talked about how they might be the future of asynchronous programming. And then I wrote this:

There is more to say about generators… But I think this post is long and bewildering enough for now. Like generators themselves, we should pause, and take up the rest another time.

Now is the time.

You can find part 1 of this article here. It’s probably best to read that before reading this. Go on, it’s fun. It’s… a little long and bewildering. But there’s a talking cat!

A quick revue

Last time, we focused on the basic behavior of generators. It’s a little strange, perhaps, but not hard to understand. A generator-function is a lot like a regular function. The main difference is that the body of a generator-function doesn’t run all at once. It runs a little bit at a time, pausing each time execution reaches a yield expression.

There’s a detailed explanation in part 1, but we never did a thorough worked example of how all the parts come together. Let’s do that now.

<pre>
function* someWords() {
yield "hello";
yield "world";
}

for (var word of someWords()) {
alert(word);
}
</pre>

This script is straightforward enough. But if you could observe everything that happens here, as though all the various bits of code were characters in a play, that would be a rather different script. It might go something like this:

SCENE - INTERIOR COMPUTER, DAY

FOR LOOP stands alone onstage, wearing a hard hat and
carrying a clipboard, all business.

                          FOR LOOP
                         (calling)
                        someWords()!
                              
The GENERATOR appears: a tall, brass, clockwork gentleman.
It looks friendly enough, but it's still as a statue.

                          FOR LOOP
               (clapping her hands smartly)
           All right! Let's get some stuff done.
                     (to the generator)
                          .next()!

The GENERATOR springs to life.

                         GENERATOR
               {value: "hello", done: false}

It freezes in a goofy pose.

                          FOR LOOP
                           alert!

Enter ALERT at a sprint, wide-eyed and breathless. We sense
he's always like that.

                          FOR LOOP
                   Tell the user "hello".

ALERT turns around and sprints offstage.

                           ALERT
                   (offstage, screaming)
                      Stop everything!
          The web page at hacks.mozilla.org says,
                          "hello"!

A few seconds' pause, then ALERT races back on, crossing all
the way over to FOR LOOP and skidding to a halt.

                           ALERT
                     The user says OK.

                          FOR LOOP
               (clapping her hands smartly)
           All right! Let's get some stuff done.
              (turning back to the GENERATOR)
                          .next()!

The GENERATOR again springs to life.

                         GENERATOR
               {value: "world", done: false}

It freezes in a different goofy pose.

                          FOR LOOP
                           alert!

                           ALERT
                     (already running)
                           On it!
                   (offstage, screaming)
                      Stop everything!
          The web page at hacks.mozilla.org says,
                          "world"!

Again, a pause, then ALERT trudges back onstage, suddenly
crestfallen.

                           ALERT
               The user says OK again, but...
             but please prevent this page from
               creating additional dialogues.

He exits, pouting.

                          FOR LOOP
               (clapping her hands smartly)
           All right! Let's get some stuff done.
              (turning back to the GENERATOR)
                          .next()!

The GENERATOR springs to life a third time.

                         GENERATOR
                       (with dignity)
               {value: undefined, done: true}

Its head comes to rest on its chest and the lights go out of
its eyes. It will never move again.

                          FOR LOOP
                  Time for my lunch break.

She exits.

After a while, the GARBAGE COLLECTOR enters, picks up the
lifeless GENERATOR, and carries it offstage.

All right—it’s not exactly Hamlet. But you get the picture.

As you can see in the play, when a generator object first appears, it is paused. It wakes up and runs for a bit each time its .next() method is called.

The action is synchronous and single-threaded. Note that only one of these characters is actually doing anything at any given time. The characters never interrupt each other or talk over one another. They take turns speaking, and whoever’s talking can go on as long as they want. (Just like Shakespeare!)

And some version of this drama unfolds each time a generator is fed to a forof loop. There is always this sequence of .next() method calls that do not appear anywhere in your code. Here I’ve put it all onstage, but for you and your programs, all this will happen behind the scenes, because generators and the forof loop were designed to work together, via the iterator interface.

So to summarize everything up to this point:

  • Generator objects are polite brass robots that yield values.
  • Each robot’s programming consists of a single chunk of code: the body of the generator function that created it.

How to shut down a generator

Generators have several fiddly extra features that I didn’t cover in part 1:

  • generator.return()
  • the optional argument to generator.next()
  • generator.throw(error)
  • yield*

I skipped them mainly because without understanding why those features exist, it’s hard to care about them, much less keep them all straight in your head. But as we think more about how our programs will use generators, we’ll see the reasons.

Here’s a pattern you’ve probably used at some point:

<pre>
function doThings() {
setup();
try {
// ... do some things ...
} finally {
cleanup();
}
}

doThings();
</pre>

The cleanup might involve closing connections or files, freeing system resources, or just updating the DOM to turn off an “in progress” spinner. We want this to happen whether our work finishes successfully or not, so it goes in a finally block.

How would this look in a generator?

<pre>
function* produceValues() {
setup();
try {
// ... yield some values ...
} finally {
cleanup();
}
}

for (var value of produceValues()) {
work(value);
}
</pre>

This looks all right. But there is a subtle issue here: the call work(value) isn’t inside the try block. If it throws an exception, what happens to our cleanup step?

Or suppose the forof loop contains a break or return statement. What happens to the cleanup step then?

It executes anyway. ES6 has your back.

When we first discussed iterators and the forof loop, we said the iterator interface contains an optional .return() method which the language automatically calls whenever iteration exits before the iterator says it’s done. Generators support this method. Calling myGenerator.return() causes the generator to run any finally blocks and then exit, just as if the current yield point had been mysteriously transformed into a return statement.

Note that the .return() is not called automatically by the language in all contexts, only in cases where the language uses the iteration protocol. So it is possible for a generator to be garbage collected without ever running its finally block.

How would this feature play out on stage? The generator is frozen in the middle of a task that requires some setup, like building a skyscraper. Suddenly someone throws an error! The for loop catches it and sets it aside. She tells the generator to .return(). The generator calmly dismantles all its scaffolding and shuts down. Then the for loop picks the error back up, and normal exception handling continues.

Generators in charge

So far, the conversations we’ve seen between a generator and its user have been pretty one-sided. To break with the theater analogy for a second:

(A fake screenshot of iPhone text messages between a generator and its user, with the user just saying 'next' repeatedly and the generator replying with values.)

The user is in charge. The generator does its work on demand. But this isn’t the only way to program with generators.

In part 1, I said that generators could be used for asynchronous programming. Things you currently do with asynchronous callbacks or promise chaining could be done with generators instead. You may have wondered how exactly that is supposed to work. Why is the ability to yield (which after all is a generator’s only special power) sufficient? After all, asynchronous code doesn’t just yield. It makes stuff happen. It calls for data from files and databases. It fires off requests to servers. And then it returns to the event loop to wait for those asynchronous processes to finish. How exactly will generators do this? And without callbacks, how does the generator receive data from those files and databases and servers when it comes in?

To start working toward the answer, consider what would happen if we just had a way for the .next() caller to pass a value back into the generator. With just this one change, we could have a whole new kind of conversation:

(A fake screenshot of iPhone text messages between a generator and its caller; each value the generator yields is an imperious demand, and the caller passes whatever the generator wants as an argument the next time it calls .next().)

And a generator’s .next() method does in fact take an optional argument, and the clever bit is that the argument then appears to the generator as the value returned by the yield expression. That is, yield isn’t a statement like return; it’s an expression that has a value, once the generator resumes.

  var results = yield getDataAndLatte(request.areaCode);

This does a lot of things for a single line of code:

  • It calls getDataAndLatte(). Let’s say that function returns the string "get me the database records for area code..." that we saw in the screenshot.
  • It pauses the generator, yielding the string value.
  • At this point, any amount of time could pass.
  • Eventually, someone calls .next({data: ..., coffee: ...}). We store that object in the local variable results and continue on the next line of code.

To show that in context, here’s code for the entire conversation shown above:

<pre>
function* handle(request) {
var results = yield getDataAndLatte(request.areaCode);
results.coffee.drink();
var target = mostUrgentRecord(results.data);
yield updateStatus(target.id, "ready");
}
</pre>

Note how yield still just means exactly what it meant before: pause the generator and pass a value back to the caller. But how things have changed! This generator expects very specific supportive behavior from its caller. It seems to expect the caller to act like an administrative assistant.

Ordinary functions are not usually like that. They tend to exist to serve their caller’s needs. But generators are code you can have a conversation with, and that makes for a wider range of possible relationships between generators and their callers.

What might this administrative assistant generator-runner look like? It doesn’t have to be all that complicated. It might look like this.

<pre>
function runGeneratorOnce(g, result) {
var status = g.next(result);
if (status.done) {
return; // phew!
}

// The generator has asked us to fetch something and
// call it back when we're done.
doAsynchronousWorkIncludingEspressoMachineOperations(
status.value,
(error, nextResult) => runGeneratorOnce(g, nextResult));
}
</pre>

To get the ball rolling, we would have to create a generator and run it once, like this:

  runGeneratorOnce(handle(request), undefined);

In May, I mentioned Q.async() as an example of a library that treats generators as asynchronous processes and automatically runs them as needed. runGeneratorOnce is that sort of thing. In practice, generator will not yield strings spelling out what they need the caller to do. They will probably yield Promise objects.

If you already understand promises, and now you understand generators, you might want to try modifying runGeneratorOnce to support promises. It’s a difficult exercise, but once you’re done, you’ll be able to write complex asynchronous algorithms using promises as straight-line code, not a .then() or a callback in sight.

How to blow up a generator

Did you notice how runGeneratorOnce handles errors? It ignores them!

Well, that’s not good. We would really like to report the error to the generator somehow. And generators support this too: you can call generator.throw(error) rather than generator.next(result). This causes the yield expression to throw. Like .return(), the generator will typically be killed, but if the current yield point is in a try block, then catch and finally blocks are honored, so the generator may recover.

Modifying runGeneratorOnce to make sure .throw() gets called appropriately is another great exercise. Keep in mind that exceptions thrown inside generators are always propagated to the caller. So generator.throw(error) will throw error right back at you unless the generator catches it!

This completes the set of possibilities when a generator reaches a yield expression and pauses:

  • Someone may call generator.next(value). In this case, the generator resumes execution right where it left off.
  • Someone may call generator.return(), optionally passing a value. In this case, the generator does not resume whatever it was doing. It executes finally blocks only.
  • Someone may call generator.throw(error). The generator behaves as if the yield expression were a call to a function that threw error.
  • Or, maybe nobody will do any of those things. The generator might stay frozen forever. (Yes, it is possible for a generator to enter a try block and simply never execute the finally block. A generator can even be reclaimed by the garbage collector while it’s in this state.)

This is not much more complicated than a plain old function call. Only .return() is really a new possibility.

In fact, yield has a lot in common with function calls. When you call a function, you’re temporarily paused, right? The function you called is in control. It might return. It might throw. Or it might just loop forever.

Generators working together

Let me show off one more feature. Suppose we write a simple generator-function to concatenate two iterable objects:

<pre>
function* concat(iter1, iter2) {
for (var value of iter1) {
yield value;
}
for (var value of iter2) {
yield value;
}
}
</pre>

ES6 provides a shorthand for this:

<pre>
function* concat(iter1, iter2) {
yield* iter1;
yield* iter2;
}

A plain yield expression yields a single value; a yield* expression consumes an entire iterator and yields all values.

The same syntax also solves another funny problem: the problem of how to call a generator from within a generator. In ordinary functions, we can scoop up a bunch of code from one function and refactor it into a separate function, without changing behavior. Obviously we’ll want to refactor generators too. But we’ll need a way to call the factored-out subroutine and make sure that every value we were yielding before is still yielded, even though it’s a subroutine that’s producing those values now. yield* is the way to do that.

<pre>
function* factoredOutChunkOfCode() { ... }

function* refactoredFunction() {
...
yield* factoredOutChunkOfCode();
...
}
</pre>

Think of one brass robot delegating subtasks to another. You can see how important this idea is for writing large generator-based projects and keeping the code clean and organized, just as functions are crucial for organizing synchronous code.

Exeunt

Well, that’s it for generators! I hope you enjoyed that as much as I did, too. It’s good to be back.

Next week, we’ll talk about yet another mind-blowing feature that’s totally new in ES6, a new kind of object so subtle, so tricky, that you may end up using one without even knowing it’s there. Please join us next week for a look at ES6 proxies in depth.

About Jason Orendorff

More articles by Jason Orendorff…


8 comments

  1. Awal

    Awesome, very nice read. Loved the Shakespeare style play!

    Been waiting for the article on Proxies since the beginning. I hope it gets some Reflect API love as well :)

    Cheers, and thanks for the awesome write-ups!

    July 9th, 2015 at 22:43

  2. bystander

    `return()` makes sense, as you need a way to clean up if you abruptly abort an iteration.

    But the other two? `next(value)` and `throw(ex)`? To me they really feel like that were slapped on this spec to enable easy async code on top of ES6 generators. Is there any other good use case? I just don’t see the link between iterating a sequence and injecting data back into it, especially not exceptions.

    Given that await/async may be coming in ES7 I wonder if those extra hacks are really justified.

    Or maybe generators are poorly named and they should have been called coroutines or something else? They are named as if their purpose is to iterate sequences, but they behave as if they want to be so much more.

    July 10th, 2015 at 13:58

  3. Flo

    Thank you soo much for this series! I always enjoy reading and learning from those articles!

    July 11th, 2015 at 04:34

  4. Michael

    Thank you so much for this wealth of info, awesome read!

    July 14th, 2015 at 12:44

  5. Mörre

    I “get” generators and promises, I’ve experimented with them. Then I decided to use them a lot less than some of these posts make seem desirable. Especially the examples here where a generator (just look at that *word*! it has meaning) is pressed into service it sure is able to do – but who is going to understand that code 2 years later?

    Too many of those excited examples about all the things you *can* (but should you?) do with the new stuff in ES 2015 are filling the blogs these days. It’s going to be a nightmare when easily influenced people think that they have to show they are part of the “in-crowd” by pushing that stuff into service. This yielding in the middle of a function to do something else and then returning with a value to me doesn’t sound like something I want to use unless I’m really really sure it makes sense and is easy to understand (in the context of 100,000 other lines of code that all crave for attention and understanding).

    Something promises, generators and certain kinds of “functional” programming style have in common is that there is a lots of stuff going on under the hood that is not obvious. Test it: Let 100 programmers explain what is actually happening (in the machine) in all those nifty examples. And I mean those who have actually already used them (in any language).

    July 16th, 2015 at 06:57

    1. Jason Orendorff

      Yeah, I should have done a better job motivating all this. The real benefit here is, we’re going to fix async programming.

      Currently most people are still doing async programming using callbacks. And it’s kind of a disaster. The code is a mess, errors get dropped, debugging is impossible. Using promises instead can reduce the amount of code by a third, and they make error handling much easier to get right. Using generators with promises will be a similar step up.

      I should have shown concrete examples of that. There was one in part 1, but I didn’t go very deep.

      This series is called “ES6 In Depth”, and the goal is to get way down into these features and explain exactly what’s going on and why. But JS programmers don’t have to understand all the internals to be productive, any more than you need to understand all the internals of React or Backbone.

      July 16th, 2015 at 08:35

      1. Sammy

        Well said about async programming using callbacks I couldn’t agree more its time to replace them for good.

        Nice article useful information thank you Jason.

        July 21st, 2015 at 21:43

  6. bystander

    @Jason Orendorff OK, so basically you’re acknowledging what I wrote above? Those non-sensical additions to “yield” are meant to fix async programming?

    Was it really necessary? We’ll have “async/await” in ES2016, which completely hides the inner workings of this. Did we need to dirty up the Generators in the meantime? Given that all this is going to be transpiled down to ES5 for some time anyway I’d guess the answer is no.

    Look at a language like C#. It has a clean “yield” (without fancy ways to pass data back in) and “await”, does it really need more?

    July 20th, 2015 at 14:13

Comments are closed for this article.