Diving Deeper With ES6 Generators

By  on  

If you're still unfamiliar with ES6 generators, first go read and play around with the code in "Part 1: The Basics Of ES6 Generators". Once you think you've got the basics down, now we can dive into some of the deeper details.

Error Handling

One of the most powerful parts of the ES6 generators design is that the semantics of the code inside a generator are synchronous, even if the external iteration control proceeds asynchronously.

That's a fancy/complicated way of saying that you can use simple error handling techniques that you're probably very familiar with -- namely the try..catch mechanism.

For example:

function *foo() {
    try {
        var x = yield 3;
        console.log( "x: " + x ); // may never get here!
    }
    catch (err) {
        console.log( "Error: " + err );
    }
}

Even though the function will pause at the yield 3 expression, and may remain paused an arbitrary amount of time, if an error gets sent back to the generator, that try..catch will catch it! Try doing that with normal async capabilities like callbacks. :)

But, how exactly would an error get sent back into this generator?

var it = foo();

var res = it.next(); // { value:3, done:false }

// instead of resuming normally with another `next(..)` call,
// let's throw a wrench (an error) into the gears:
it.throw( "Oops!" ); // Error: Oops!

Here, you can see we use another method on the iterator -- throw(..) -- which "throws" an error into the generator as if it had occurred at the exact point where the generator is currently yield-paused. The try..catch catches that error just like you'd expect!

Note: If you throw(..) an error into a generator, but no try..catch catches it, the error will (just like normal) propagate right back out (and if not caught eventually end up as an unhandled rejection). So:

function *foo() { }

var it = foo();
try {
    it.throw( "Oops!" );
}
catch (err) {
    console.log( "Error: " + err ); // Error: Oops!
}

Obviously, the reverse direction of error handling also works:

function *foo() {
    var x = yield 3;
    var y = x.toUpperCase(); // could be a TypeError error!
    yield y;
}

var it = foo();

it.next(); // { value:3, done:false }

try {
    it.next( 42 ); // `42` won't have `toUpperCase()`
}
catch (err) {
    console.log( err ); // TypeError (from `toUpperCase()` call)
}

Delegating Generators

Another thing you may find yourself wanting to do is call another generator from inside of your generator function. I don't just mean instantiating a generator in the normal way, but actually delegating your own iteration control to that other generator. To do so, we use a variation of the yield keyword: yield * ("yield star").

Example:

function *foo() {
    yield 3;
    yield 4;
}

function *bar() {
    yield 1;
    yield 2;
    yield *foo(); // `yield *` delegates iteration control to `foo()`
    yield 5;
}

for (var v of bar()) {
    console.log( v );
}
// 1 2 3 4 5

Just as explained in part 1 (where I used function *foo() { } instead of function* foo() { }), I also use yield *foo() here instead of yield* foo() as many other articles/docs do. I think this is more accurate/clear to illustrate what's going on.

Let's break down how this works. The yield 1 and yield 2 send their values directly out to the for..of loop's (hidden) calls of next(), as we already understand and expect.

But then yield* is encountered, and you'll notice that we're yielding to another generator by actually instantiating it (foo()). So we're basically yielding/delegating to another generator's iterator -- probably the most accurate way to think about it.

Once yield* has delegated (temporarily) from *bar() to *foo(), now the for..of loop's next() calls are actually controlling foo(), thus the yield 3 and yield 4 send their values all the way back out to the for..of loop.

Once *foo() is finished, control returns back to the original generator, which finally calls the yield 5.

For simplicity, this example only yields values out. But of course, if you don't use a for..of loop, but just manually call the iterator's next(..) and pass in messages, those messages will pass through the yield* delegation in the same expected manner:

function *foo() {
    var z = yield 3;
    var w = yield 4;
    console.log( "z: " + z + ", w: " + w );
}

function *bar() {
    var x = yield 1;
    var y = yield 2;
    yield *foo(); // `yield*` delegates iteration control to `foo()`
    var v = yield 5;
    console.log( "x: " + x + ", y: " + y + ", v: " + v );
}

var it = bar();

it.next();      // { value:1, done:false }
it.next( "X" ); // { value:2, done:false }
it.next( "Y" ); // { value:3, done:false }
it.next( "Z" ); // { value:4, done:false }
it.next( "W" ); // { value:5, done:false }
// z: Z, w: W

it.next( "V" ); // { value:undefined, done:true }
// x: X, y: Y, v: V

Though we only showed one level of delegation here, there's no reason why *foo() couldn't yield* delegate to another generator iterator, and that to another, and so on.

Another "trick" that yield* can do is receive a returned value from the delegated generator.

function *foo() {
    yield 2;
    yield 3;
    return "foo"; // return value back to `yield*` expression
}

function *bar() {
    yield 1;
    var v = yield *foo();
    console.log( "v: " + v );
    yield 4;
}

var it = bar();

it.next(); // { value:1, done:false }
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // "v: foo"   { value:4, done:false }
it.next(); // { value:undefined, done:true }

As you can see, yield *foo() was delegating iteration control (the next() calls) until it completed, then once it did, any return value from foo() (in this case, the string value "foo") is set as the result value of the yield* expression, to then be assigned to the local variable v.

That's an interesting distinction between yield and yield*: with yield expressions, the result is whatever is sent in with the subsequent next(..), but with the yield* expression, it receives its result only from the delegated generator's return value (since next(..) sent values pass through the delegation transparently).

You can also do error handling (see above) in both directions across a yield* delegation:

function *foo() {
    try {
        yield 2;
    }
    catch (err) {
        console.log( "foo caught: " + err );
    }

    yield; // pause

    // now, throw another error
    throw "Oops!";
}

function *bar() {
    yield 1;
    try {
        yield *foo();
    }
    catch (err) {
        console.log( "bar caught: " + err );
    }
}

var it = bar();

it.next(); // { value:1, done:false }
it.next(); // { value:2, done:false }

it.throw( "Uh oh!" ); // will be caught inside `foo()`
// foo caught: Uh oh!

it.next(); // { value:undefined, done:true }  --> No error here!
// bar caught: Oops!

As you can see, the throw("Uh oh!") throws the error through the yield* delegation to the try..catch inside of *foo(). Likewise, the throw "Oops!" inside of *foo() throws back out to *bar(), which then catches that error with another try..catch. Had we not caught either of them, the errors would have continued to propagate out as you'd normally expect.

Summary

Generators have synchronous execution semantics, which means you can use the try..catch error handling mechanism across a yield statement. The generator iterator also has a throw(..) method to throw an error into the generator at its paused position, which can of course also be caught by a try..catch inside the generator.

yield* allows you to delegate the iteration control from the current generator to another one. The result is that yield* acts as a pass-through in both directions, both for messages as well as errors.

But, one fundamental question remains unanswered so far: how do generators help us with async code patterns? Everything we've seen so far in these two articles is synchronous iteration of generator functions.

The key will be to construct a mechanism where the generator pauses to start an async task, and then resumes (via its iterator's next() call) at the end of the async task. We will explore various ways of going about creating such asynchronicity-control with generators in the next article. Stay tuned!

Kyle Simpson

About Kyle Simpson

Kyle Simpson is an Open Web Evangelist from Austin, TX, who's passionate about all things JavaScript. He's an author, workshop trainer, tech speaker, and OSS contributor/leader.

Recent Features

  • By
    How I Stopped WordPress Comment Spam

    I love almost every part of being a tech blogger:  learning, preaching, bantering, researching.  The one part about blogging that I absolutely loathe:  dealing with SPAM comments.  For the past two years, my blog has registered 8,000+ SPAM comments per day.  PER DAY.  Bloating my database...

  • By
    Page Visibility API

    One event that's always been lacking within the document is a signal for when the user is looking at a given tab, or another tab. When does the user switch off our site to look at something else? When do they come back?

Incredible Demos

Discussion

  1. very nice, waiting for your next post about async function

  2. Any iterable object can be passed to yield*.

    yield* iterable
    
    function* gen(){
      yield* ["a", "b", "c"]
    }
    
    gen().next() // { value:"a", done:false }
    
    • Yes, but I didn’t want to confuse the topic above. But that’s exactly why I did say:

      > So we’re basically yielding/delegating to another generator’s iterator — probably the most accurate way to think about it.

      yield* delegates to another iterator. That’s most commonly to another generator’s iterator, but as you’ve pointed out it can be to any iterable (an object that exposes an iterator).

  3. Benjamin Gruenbaum

    Very nicely written Kyle :) The only thing I’d change is not throw strings.

  4. So the throw inside the delegated generator acts like a return, i.e. it terminates the delegation and the yield* foo() expression evaluates to the thrown error, correct?

    • > So the “throw” inside the delegated generator acts like a “return”, i.e. it terminates the delegation

      Sorta. It does terminate, yes, as any throw would. But it’s an abrupt termination, not a normal one.

      > and the “yield* foo()” expression evaluates to the thrown error, correct?

      Nope. The yield* expression can only receive values from a true return (normal termination). If the delegate generator throws an error, this abrupt termination results in the error being “thrown” in the calling generator, which would terminate the `yield* expression prematurely, preventing you from observing any resulting value.

      As shown, the way to get the error is to use a try..catch around the yield*.

    • it is interesting how bar() “waits” till the following it.next() to actually handle the error thrown in foo(). Wondering if I can send some value in this was it.next() to the bar() iterator that actually helps me to handle the error.

  5. this looks intimidating for a new bie like me but thanks for the share by the way.

  6. really good article! thanks!

    not really sure delegating iterators is a good thing. from my code, I can think I am running bar(), when actually at some point I might be running something different!!!

    i can see the delegation control and the “yield” keyword can be use as some sort of debugging mechanism!

    now I assume yield can send in/out any kind of data including anonymous functions. What would happen to the anonymous function scope in such case?

  7. > not really sure delegating iterators is a good thing.

    This post didn’t make it terribly obvious why this is useful, but I believe you’ll see why in the fourth post in the series, when we talk about generator coroutines.

    Hint: generator delegation via yield* is basically the same thing as why you’d take a single function and factor out parts of it to another function: code maintainability, separation of concerns, etc.

    > from my code, I can think I am running bar(), when actually at some point I might be running something different!!!

    Isn’t that same thing true of any function call? If you call foo(), you have no idea from “your code” what it calls. How is yield* any different in that respect than just normal function calls?

    > now I assume yield can send in/out any kind of data including anonymous functions.

    Sure.

    > What would happen to the anonymous function scope in such case?

    functions maintain their lexical scope (called closure) regardless of how or where they are passed, so sending a function through a generator wouldn’t be any different. :)

  8. krish

    In your last example, just before the summary, the last it.next() shouldn’t be there. The code should end with

    it.throw( "Uh oh!" );// will be caught inside foo()
    //foo caught: Uh oh!
    //bar caught: Oops! { value:undefined, done:true }
    • Actually, the it.next() was intended. What’s wrong is that I accidentally omitted an intended yield; // pause after the try..catch in the *foo() generator. I’ve corrected the code snippet. Thanks for the good “catch”! :)

  9. Very interesting article! Thanks for sharing; now I finally understand the basics of generators :)

Wrap your code in <pre class="{language}"></pre> tags, link to a GitHub gist, JSFiddle fiddle, or CodePen pen to embed!