JavaScript Generators Magic and Their Use Case: Everything Yield Need To Know

JavaScript generators is an amazing and brilliant concept; yet it’s very confusing when to use them. They let us write infinite loops that terminate. The yield values from functions before they finish and build lazy data structures. The magic lies in the ability of generators functions to be paused and then resumed again. But before answering ‘how’ and ‘where’; let’s see ‘what’ generator function is.

Regular generator functions are basically a cross between the Iterator and Observer patterns. A generator is a pause-able function that you can “step” through by calling .next(). We can pull a value out of a generator multiple times with .next(), or push a value into the same function multiple times with .next(valueToPush). This dual interface allows us to imitate both an Iterator and Observer with the same syntax.

Regular functions return only one, single value (or nothing). Generators can return multiple values, one after another, on-demand. They work great with iterables, allowing to create data streams. To create a generator, we need a special syntax construct: function*. It looks like this:

function* generateSequence() {
  yield 'one';
  yield 'two';
  return 'three';
}

Generator functions behave differently from regular ones. When such function is called, it doesn’t run its code. Instead it returns a special object, called “generator object”, to manage the execution. The main method of a generator is next(). When called, it runs the execution until the nearest yield<value> statement. Then the function execution pauses, and the yielded value is returned to the outer code.

The result of next() is always an object with two properties:

  • value: the yielded value.
  • done: true if the function code has finished, otherwise false.
function* generateSequence() {
  yield 'one';
  yield 'two';
  return 'three';
}
let generator = generateSequence();
let output1 = generator.next();
alert(JSON.stringify(output1)); // {value: 'one', done: false}
let output2 = generator.next();
alert(JSON.stringify(output2)); // {value: 'two', done: false}
let output3 = generator.next();
alert(JSON.stringify(output3)); // {value: 'three', done: true}
alert(JSON.stringify(output3)); // {value: undefined, done: true}
Now let’s turn our focus on ‘where’ –
  • Lets say we want to play and pause loops if someone hit a pause button on an animation. Generators can be used with this for loops which need to be paused and resumed at a later time.
  • We want an infinitely looping array for things like carousels or lists of data which wrap around. Generators can be used for infinitely looping over an array and having it reset to the beginning once it’s done.
  • We want to iterate over objects to check if any of its properties include a specific value. We use generators by creating iterables to use in ‘for of ‘ loops from non-iterable objects using [Symbol.Iterator]; get the array and then check with .includes().
  • to loop through structures where we’d normally need to keep track of multiple variables at a time.

Let’s see some ‘how’ of these ‘where’.

#1: looping arrays to pause and resume

function* makeGeneratorLoop(arr) {
  for (const value of arr) {
    yield value;
  }
}
const genExample = makeGeneratorLoop([0, 1, 9, 8, 6, 2]);
console.log(genExample.next());

#2: Infinitely looping array

function* makeGeneratorLoop(arr) {
  for (let i = 0;; i++) {
    if (i === arr.length) i = 0;
    yield arr[i];
  }
}
const genExample = makeGeneratorLoop([0, 1, 9, 8, 6, 2]);
console.log(genExample.next());

#3: Iterable from Object

const myObj = {
  name: 'Asif', 
  Job: 'Developer', 
  age: 27
}
myObj[Symbol.iterator] = function* () {
  for (const prop in this) {
    yield this[prop];
  }
}
console.log([...myObj]); // > ["Asif", "Developer", 27]
for (const val of myObj) {
  console.log(val)
}
Javascript Generators Use Case?

In a normal function, there is only one entry point – the invocation of the function itself. A generator allows us to pause the execution of a function and resume it later. Generators are useful when dealing with iterators and can simplify the asynchronous nature of JavaScript.

generators are a really powerful tool that facilitates cleaner code — especially when it comes to any kind of async behavior. A side benefit of generators is easier testing. For example, we could test a program by writing a different interpreter, that asserts the yielded commands without executing them.

How does yield work JavaScript?

The yield keyword pauses generator function execution and retains the value of the expression after the yield keyword is returned to the generator’s caller. yield can only be called directly from the generator function that contains it. It cannot be called from nested functions or from callbacks. Once paused on a yield expression, the generator’s code execution remains paused until the generator’s next() method is called. Each time the generator’s next() method is called, the generator resumes execution.

What other cases you have used javascript generators? Let us know by commenting down below!

javascript generators use case

Generators are iterable

generators are iterable. We can loop over their values using for..of:

function* generateSequence() {
  yield 1;
  yield 2;
  return 3;
}

let generator = generateSequence();

for(let value of generator) {
  alert(value); // 1, then 2
}

Looks a lot nicer than calling .next().value, right?
But please note: the example above shows 1, then 2, and that’s all. It doesn’t show 3!

It’s because for..of iteration ignores the last value, when done: true. So, if we want all results to be shown by for..of, we must return them with yield:

function* generateSequence() {
  yield 1;
  yield 2;
  yield 3;
}

let generator = generateSequence();

for(let value of generator) {
  alert(value); // 1, then 2, then 3
}

As generators are iterable, we can call all related functionality, e.g. the spread syntax ...:

function* generateSequence() {
  yield 1;
  yield 2;
  yield 3;
}

let sequence = [0, ...generateSequence()];

alert(sequence); // 0, 1, 2, 3

In the code above, ...generateSequence() turns the iterable generator object into an array of items.

Generator composition

Generator composition is a special feature of generators that allows to transparently “embed” generators in each other.

For instance, we have a function that generates a sequence of numbers:

function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) yield i;
}

Now we’d like to reuse it to generate a more complex sequence:

  • first, digits 0..9 (with character codes 48…57),
  • followed by uppercase alphabet letters A..Z (character codes 65…90)
  • followed by lowercase alphabet letters a..z (character codes 97…122)

We can use this sequence e.g. to create passwords by selecting characters from it (could add syntax characters as well), but let’s generate it first.

In a regular function, to combine results from multiple other functions, we call them, store the results, and then join at the end.

For generators, there’s a special yield* syntax to “embed” (compose) one generator into another.

The composed generator:

function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) yield i;
}

function* generatePasswordCodes() {

  // 0..9
  yield* generateSequence(48, 57);

  // A..Z
  yield* generateSequence(65, 90);

  // a..z
  yield* generateSequence(97, 122);

}

let str = '';

for(let code of generatePasswordCodes()) {
  str += String.fromCharCode(code);
}

alert(str); // 0..9A..Za..z

The yield* directive delegates the execution to another generator. This term means that yield* gen iterates over the generator gen and transparently forwards its yields outside. As if the values were yielded by the outer generator.

The result is the same as if we inlined the code from nested generators:

function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) yield i;
}

function* generateAlphaNum() {

  // yield* generateSequence(48, 57);
  for (let i = 48; i <= 57; i++) yield i;

  // yield* generateSequence(65, 90);
  for (let i = 65; i <= 90; i++) yield i;

  // yield* generateSequence(97, 122);
  for (let i = 97; i <= 122; i++) yield i;

}

let str = '';

for(let code of generateAlphaNum()) {
  str += String.fromCharCode(code);
}

alert(str); // 0..9A..Za..z

A generator composition is a natural way to insert a flow of one generator into another. It doesn’t use extra memory to store intermediate results.

javascript

“yield” is a two-way street

Until this moment, generators were similar to iterable objects, with a special syntax to generate values. But in fact they are much more powerful and flexible. That’s because yield is a two-way street: it not only returns the result to the outside, but also can pass the value inside the generator. To do so, we should call generator.next(arg), with an argument. That argument becomes the result of yield.

Let’s see an example:

function* gen() {
  // Pass a question to the outer code and wait for an answer
  let result = yield "2 + 2 = ?"; // (*)

  alert(result);
}

let generator = gen();

let question = generator.next().value; // <-- yield returns the value

generator.next(4); // --> pass the result into the generator

  1. The first call generator.next() should be always made without an argument (the argument is ignored if passed). It starts the execution and returns the result of the first yield "2+2=?". At this point the generator pauses the execution, while staying on the line (*).
  2. Then, as shown at the picture above, the result of yield gets into the question variable in the calling code.
  3. On generator.next(4), the generator resumes, and 4 gets in as the result: let result = 4.

Please note, the outer code does not have to immediately call next(4). It may take time. That’s not a problem: the generator will wait.
For instance:

// resume the generator after some time
setTimeout(() => generator.next(4), 1000);

As we can see, unlike regular functions, a generator and the calling code can exchange results by passing values in next/yield.
To make things more obvious, here’s another example, with more calls:

function* gen() {
  let ask1 = yield "2 + 2 = ?";

  alert(ask1); // 4

  let ask2 = yield "3 * 3 = ?"

  alert(ask2); // 9
}

let generator = gen();

alert( generator.next().value ); // "2 + 2 = ?"

alert( generator.next(4).value ); // "3 * 3 = ?"

alert( generator.next(9).done ); // true

  1. The first .next() starts the execution… It reaches the first yield.
  2. The result is returned to the outer code.
  3. The second .next(4) passes 4 back to the generator as the result of the first yield, and resumes the execution.
  4. …It reaches the second yield, that becomes the result of the generator call.
  5. The third next(9) passes 9 into the generator as the result of the second yield and resumes the execution that reaches the end of the function, so done: true.

It’s like a “ping-pong” game. Each next(value) (excluding the first one) passes a value into the generator, that becomes the result of the current yield, and then gets back the result of the next yield.

generator.throw

As we observed in the examples above, the outer code may pass a value into the generator, as the result of yield.

…But it can also initiate (throw) an error there. That’s natural, as an error is a kind of result.

To pass an error into a yield, we should call generator.throw(err). In that case, the err is thrown in the line with that yield.

For instance, here the yield of "2 + 2 = ?" leads to an error:

function* gen() {
  try {
    let result = yield "2 + 2 = ?"; // (1)

    alert("The execution does not reach here, because the exception is thrown above");
  } catch(e) {
    alert(e); // shows the error
  }
}

let generator = gen();

let question = generator.next().value;

generator.throw(new Error("The answer is not found in my database")); // (2)

The error, thrown into the generator at line (2) leads to an exception in line (1) with yield. In the example above, try..catch catches it and shows it.

If we don’t catch it, then just like any exception, it “falls out” the generator into the calling code.

The current line of the calling code is the line with generator.throw, labelled as (2). So we can catch it here, like this:

function* generate() {
  let result = yield "2 + 2 = ?"; // Error in this line
}

let generator = generate();

let question = generator.next().value;

try {
  generator.throw(new Error("The answer is not found in my database"));
} catch(e) {
  alert(e); // shows the error
}

If we don’t catch the error there, then, as usual, it falls through to the outer calling code (if any) and, if uncaught, kills the script.

Leave a Comment