JS Interview #11 —JavaScript Generator

Mighty Ghost Hack
Mighty ghost hack
Published in
6 min readJul 4, 2023
JavaScript Generator

In this article, we will discuss JavaScript Generators with the help of some examples and how to use them effectively.

In JavaScript, a regular function is executed continuously and cannot pause midway.

function regularFunction() {
console.log('Lets');
console.log('understand');
console.log('regular function execution');
}

regularFunction();

// Output
// Lets
// understand
// regular function execution

The regularFunction() the function executes from top to bottom. The only way to exit regularFunction() is by returning from it or throwing an error. If you invoke the regularFunction() function again, it will start the execution from top to bottom again.

ES6 introduces a new kind of function that is different from a regular function which is known as Generators.

JavaScript Generators

A generator is a function that helps to pause a function midway and then continues from where it paused.

Regular functions return a single value or nothing, whereas generators return multiple values in a sequence, one after another.

Create JavaScript Generators

To create a generator, it is necessary to use a particular syntax construct: function* known as a generator function.

The generator function is denoted by *. You can either use function* generatorFunc() {...} or function *generatorFunc(){...} to create them.

// define a generator function
function* generator_function() {
... .. ...
}

Generators Object

function* generateSequence() {
}

// "generator function" creates "generator object"
let generator = generateSequence();
console.log(generator); // { [Generator] }

Here generateSequence() the function will return a generator object.

generateSequence() the function code execution hasn’t started yet, Now to invoke we have the main method of a generator is next(). When called, it runs the execution until the nearest yield

Yield to Pause Execution

Here function execution is paused at first yield.

function* generateSequence() {
console.log('lets');
yield;
console.log('understand');
yield;
console.log('generators');
}

// "generator function" creates "generator object"
let generator = generateSequence();
generator.next(); // lets

Let’s call generator.next() again. It resumes the code execution and returns the next yield:

function* generateSequence() {
console.log('lets');
yield;
console.log('understand');
yield;
console.log('generators');
}

// "generator function" creates "generator object"
let generator = generateSequence();
generator.next(); // lets
generator.next(); // understand

Generators next() method

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.

For example, here we create the generator and get its yielded value: { value: undefined, done: false }

function* generateSequence() {
console.log('lets');
yield;
console.log('understand');
yield;
console.log('generators');
}

// "generator function" creates "generator object"
let generator = generateSequence();
generator.next(); // lets

let val = generator.next();
console.log(val); // { value: undefined, done: false }

Here we have value as undefined because we are not passing any value with yield.

For example, here we create the generator and get its yielded value: { value: 2, done: false }

function* generateSequence() {
console.log('lets');
yield 1;
console.log('understand');
yield 2;
console.log('generators');
}

// "generator function" creates "generator object"
let generator = generateSequence();
generator.next(); // lets

let val = generator.next();
console.log(val); // { value: 2, done: false }

If you observed till now done contains false

Let’s call generator.next() again. It resumes the code execution and returns the next yield value, if any there else it will return done as true and the value should be undefined { value: undefined, done: true }

function* generateSequence() {
console.log('lets');
yield 1;
console.log('understand');
yield 2;
console.log('generators');
}

// "generator function" creates "generator object"
let generator = generateSequence();
generator.next(); // lets

let val = generator.next();
console.log(val); // { value: 2, done: false }

val = generator.next();
console.log(val); // { value: undefined, done: true }

Here is another twist, it can also capture the last return value in the next() method return object.

function* generateSequence() {
console.log('lets');
yield 1;
console.log('understand');
yield 2;
console.log('generators');
return 3;
}

// "generator function" creates "generator object"
let generator = generateSequence();
generator.next(); // lets

let val = generator.next();
console.log(val); // { value: 2, done: false }

val = generator.next();
console.log(val); // { value: 3, done: true }

Now the generator is done. We should see it done:true and process value:3 it as the final result.

Generators are iterable

As you probably already guessed looking at the next() method, 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) {
console.log(value); // 1, 2
}

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) {
console.log(value); // 1, 2, 3
}

As generators are iterable, we can call all related functionality. For example, the spread syntax ...

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

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

console.log(sequence); // [ 0, 1, 2, 3 ]

generator.return

generator.return(value) finishes the generator execution and returns the given value.

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

let generator = generateSequence();

console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.return(4)); // { value: 4, done: true }
console.log(generator.next()); // { value: undefined, done: true }

If we again use generator.return() in a completed generator, it will return that value again.

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

let generator = generateSequence();

console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.return(4)); // { value: 4, done: true }
console.log(generator.next()); // { value: undefined, done: true }
console.log(generator.return(5)); // { value: 5, done: true }

“Yield” is a Two-way Street

Generators are like iterable objects with a unique syntax for generating values. But, they are even more flexible and powerful.

So, yield is considered a two-way street: it can both return the result to the outside and pass the value inside the generator.

To implement that, you need to call the generator.next(arg) using an argument. The argument will become the result of the yield.

function* gen() {
// Pass a question to external code and wait for an answer
let result = yield "2 * 2 = ?"; // (*)
return result;
}
let generator = gen();
console.log(generator.next()); // yield returns the value
console.log(generator.next(4)); // pass the result into the generator

// Output
// { value: '2 * 2 = ?', done: false }
// { value: 4, done: true }

generator.throw

To pass an error into a yield, we should call generator.throw(err).

function* gen() {
try {
yield "Hello";
} catch (e) {
console.log(e); // shows the error
}
}

let generator = gen();
console.log(generator.next());
console.log(generator.throw(new Error("The answer is not found in my database")));

// output
// { value: 'Hello', done: false }
// Error: The answer is not found in my database
// { value: undefined, done: true }

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

Yield with finally

Here in this example, finally yield is getting called even program gets terminated by return()

It is better to wrap a block of code with the help of try catch finally block.

function* gen() {
try {
yield "Start";
yield "In Progress";
} catch (e) {
console.log(e); // shows the error
} finally {
yield "Finish"
}
}

let generator = gen();
console.log(generator.next());
console.log(generator.return());
console.log(generator.next());

// output
// { value: 'Start', done: false }
// { value: 'Finish', done: false }
// { value: undefined, done: true }

Generator Composition

Generator composition is a unique feature of generators that allows embedding generators in each other transparently.

Let’s consider a function, which generates a sequence of number

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

const genNumber = gen(5, 10);
console.log(genNumber.next()); // { value: 5, done: false }
console.log(genNumber.next()); // { value: 6, done: false }
console.log(genNumber.next()); // { value: 7, done: false }
console.log(genNumber.next()); // { value: 8, done: false }

Generators have a unique yield* syntax for composing one generator into another.

The following example shows the perfect example of generator composition by yield* keyword.

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

function* generateNumber() {
yield* gen(48, 57);
}

const genNumber = generateNumber(5, 10);
console.log(genNumber.next()); // { value: 48, done: false }
console.log(genNumber.next()); // { value: 49, done: false }
console.log(genNumber.next()); // { value: 50, done: false }
console.log(genNumber.next()); // { value: 51, done: false }

You can also use yield* in the recursion of the generator, which is another use case.

Key Points to remember

  • Normal functions return only one, single value (or nothing) and Generators return multiple values (multiple yield)
  • yield should use inside Generators only.
  • Generator syntax function* generatorFunc() or function *generatorFunc() Both syntaxes are correct.
  • A Generator function is Iterable.
  • yield* is used for calling the generator function from the existing generator. (ex. recursion with generator)
  • Generators are processes that can halt and resume execution. They are a powerful, versatile feature of JavaScript, although they are not commonly used.

--

--

Mighty Ghost Hack
Mighty ghost hack

A passionate full-stack developer, Ethical Hacker, Blogger, Youtuber and many more