I Am
Volodymyr Hudyma
<FrontEndDeveloper />
You Are Here: Home/Generators In JavaScript

Generators In JavaScript

June 01, 2020

Table Of Contents

    Generator function - a special kind of function that can stop midway and continue execution from the place where it stopped.

    This function returns a generator object, which conforms to both - iterable and iterator protocol.

    For creating a generator function, function* syntax is used.

    Inside of the function body we have yield keywords which pause the function execution.

    Every time a generator encounters this keyword, it returns the value specified after it:

    function* exampleGenerator() { 
      yield 1;
    };

    We can also return from a generator. However, return keyword finishes the generator:

    function* exampleGenerator() { 
      yield 1;
      // The generator finishes at this moment
      return;
      // The following line of code is never accessed
      yield 2;
    };

    Important note: it's not possible to create a generator using an arrow function.

    Iterable Protocol

    This protocol defines the iteration behavior of objects.

    That means the way an object iterates through its items, and also allows us to handle and customize that behavior.

    In order to learn more about it, refer to this article.

    Iterator Protocol

    This protocol defines a standard way to produce a sequence of values (either finite or infinite), and potentially a return value when all values have been generated.

    In order to learn more about it, refer to this article.

    Generator Object

    Generator object can not be instantiated using new keyword, it only can be returned from the generator function:

    function* exampleGenerator() { 
      yield 1;
      yield 2;
      yield 3;
    };
    
    const result = exampleGenerator(); 
    
    console.log(result); // Prints "Iterator [Generator] {}"

    What is the difference between the exampleGenerator and the regular function?

    Regular function can't be stopped in the middle of execution.

    The only ways to exit the function are: to use return keyword or throw an error:

    function exampleFunction() { 
      console.log("1");
      console.log("2");
      console.log("3");
      return;
      console.log("4");
    };
    
    const result = exampleFunction(); // Prints "1", "2", "3"

    Generator Methods

    Generator object provides us with 3 built-in methods:

    • Generator.prototype.next() - returns a value yielded by the yield expression:
    function* exampleGenerator() { 
      yield 1;
      yield 2;
      yield 3;
    };
    
    const result = exampleGenerator(); 
    
    console.log(result.next()); // Prints "{value: 1, done: false}"
    console.log(result.next()); // Prints "{value: 2, done: false}"
    console.log(result.next()); // Prints "{value: 3, done: false}"
    console.log(result.next()); // Prints "{value: undefined, done: true}"

    Important note: Every invocation of next returns an object of shape: {value: any, done: true|false}. When the done is true, the generator stops and won't generate any more values.

    • Generator.prototype.return() - returns given value and finishes the generator:
    function* exampleGenerator() { 
      yield 1;
      yield 2;
      yield 3;
    };
    
    const result = exampleGenerator(); 
    
    console.log(result.next()); // Prints "{value: 1, done: false}"
    console.log(result.return(4)); // Prints "{value: 4, done: true}" 
    • Generator.prototype.throw() - throws an error to the generator (finishes the generator as well, unless caught from within that generator):

    Uncaught error example

    function* exampleGenerator() { 
      yield 1;
      yield 2;
      yield 3;
    };
    
    const result = exampleGenerator(); 
    
    // Prints "{value: 1, done: false}"
    console.log(result.next()); 
    
    // Prints "Error: Something went wrong" 
    console.log(result.throw(new Error("Something went wrong"))); 

    Caught error example

    function* exampleGenerator() { 
      try {
        yield 1;
        yield 2;
        yield 3;   
      } catch(e) {
        console.log("Error caught");
      };
    };
    
    const result = exampleGenerator(); 
    
    // Prints "{value: 1, done: false}"
    console.log(result.next()); 
    
    // "Error caught" is printed
    
    // Prints "{value: undefined, done: true}"
    console.log(result.throw(new Error("Something went wrong")));

    Usage Of Generators

    When you start learning about the generators it may not be obvious where are they used, so let's take a look at the use cases:

    Implementing iterables

    Imagine that we want to create a custom iterable that returns a sequence of words.

    Consider the following method using iterators:

    const iterable = {
      [Symbol.iterator]() {
        let step = 0;
        return {
          next() {
            step++;
            if (step === 1) {
              return { value: "Hello", done: false};
            } else if (step === 2) {
              return { value: "World", done: false};
            }
            return { value: undefined, done: true };
          },
        };
      },
    };
    
    /* 
      Prints:
        "Hello"
        "World"
    */
    for (const result of iterable) {
      console.log(result);
    }

    And the same example using generators:

      function* example() {
      yield "Hello";
      yield "World";
    };
    
    const generator = example();
    
    /* 
      Prints:
        "Hello"
        "World"
    */
    for (const result of generator) {
      console.log(result);
    }

    Compare the amount of code and simplicity of both examples, which do the same thing.

    Waiting for a promise to resolve

    Let's assume we have to fetch user details using the following code, based on Promise:

    const fetchUser = () => {
        return axios("url")
          .then(user => JSON.parse(user))
          .catch(error => { console.log(error); });
    };

    The same code but using async/await:

    const fetchUser = async () => {
      try {
        const user = await axios("url");
        return JSON.parse(user);
      } catch(e) {
        console.log(error);
      }
    };

    And finally, using generators:

    function* fetchUser() {
      try {
        const user = yield axios.get("url");
        return JSON.parse(user);
      } catch(e) {
        console.log(error);
      }
    };

    Generating infinite/unique data streams

    You can use generators to create an infinite data stream of unique identifiers:

    function* idGenerator() {
      let i = 0;
      while (true) {
        yield i++;
      }
    };
    
    const id = idGenerator();
    
    console.log(id.next()); // Prints "{value: 0, done: false}"
    console.log(id.next()); // Prints "{value: 1, done: false}"
    console.log(id.next()); // Prints "{value: 2, done: false}"

    Summary

    We've covered the very basics of generators, by now you should have an understanding of what are they and how do they work.

    • Generator - function that can be suspended and resumed later on
    • Generator object is returned from the generator function, is can't be instantiated using new keyword
    • Generator has 3 built-in methods: next(), return(), throw()
    • Generators can be used for: implementing iterables, waiting for Promise to resolve, generating infinite data streams and much more
    Newsletter
    Receive all new posts directly to your e-mail
    No spam, only quality content twice a week
    Let me know what you think about this article
    Click here to write response...