Iterators and Generators. gateway to the async/await pattern :

Iterators and Generators. gateway to the async/await pattern :

ยท

11 min read

  • In the last article, we discussed javascript Promises and how they solved the problems of callback functions.
  • javascript promises are great but chaining them one by one is not very declarative.
  • it would be very cool if we are able to write asynchronous code line by line just like normal synchronous code.
  • combination of ES6 generators and promise will do just that for us. So let's learn Iterators and Generators.

Iterators:-

  • let's say you have data source(array or set) and you want to consume values of data source one at a time, for that you can use iterators. you can do that by creating a special object and calling .next() over and over on that object until the data source is consumed.

  • now let's look at how we can create the Iterator of an array.


let arr = [1,2,3,4];

let it = arr[Symbol.iterator]()

it.next()      //  {value : 1, done : false }
it.next()      //  {value : 2, done : false }
it.next()      //  {value : 3, done : false }
it.next()      //  {value : 4, done : false }
it.next()      //  {value : undefined, done : true }
  • you can create the Iterator of data-structure by calling the Symbol.iterator() on it. this function call returns an iterator.
  • On that iterator, you can call the .next() method which returns the object with value and done property.
  • you can call .next() until done property returns true.
  • Here, you can see that when the value is 4, you don't get done: true but you get done: false in the next call. because iterator returns done: true only after the data-source is fully consumed.
  • calling .next() over and over is very boring ๐Ÿ’ค. so javascript provide us for-of loop which will consume the data-source until the iterator does not returns { done: false }
const x = [1,2,3,4];
const it = x[Symbol.iterator]();

for(const item of it){
    console.log(item);
} 

// 1,2,3,4
  • with the for-of loop you can also use data-structure directly instead of creating an iterator of it but that data-structure should have Symbol.iterator method.
const x = [1,2,3,4];

for(const item of x){
  console.log(item);
}
  • Here, array has the default Symbol.iterator method, so you can use for-of on the array directly.
  • In javascript array, string, Map,Set, TypedArray has default Symbol.iterator method.
  • but not all data-structure has the Symbol.iterator method. for example, the main javascript data-type object does not have a default iterator. so In order to leverage the iterator pattern, we need to create our own iterator function.
  • To create a custom iterator javascript provides an easy and declarative pattern called generator.
  • let's let look at the code and I will explain how the generator works.

const obj = {
    a:3,
    b:4,
    c:5,
    [Symbol.iterator]:function *() {
        const keys = Object.keys(this);
        for(let i=0;i<keys.length;i++){
            yield keys[i];
        }
    }
}

const it = obj[Symbol.iterator]();

it.next()      //  {value : "a", done : false }
it.next()      //  {value : "b", done : false }
it.next()      //  {value : "c", done : false }
it.next()      //  {value : undefined, done : true }

for(const item of obj){
    console.log(item)      // a,b,c
}
  • Here, we are providing the function to the Symbol.iterator. this is required for creating the iterator but As you can see here we are creating a function with function *() {} syntax. which tells javascript that we are using the generator function.

  • second new syntax is the yield keyword. when we call .next() on iterator created by generator, generator function executes until it encounters yield, and gives value associated to the yield to the value property returned by next() function.

  • iterator will return {done: false} as long it encounters yield keyword. after that, it assumes that we have consumed our data-source and it will return the {done: true} from now on.
  • good thing about generator is that you can also create a stand-alone generator function. you don't need an object to create iterator using generator.

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

for(const item of makeRangeIterator()){
    console.log(item) // 0,1,2,3,4,5,6,7,8,9
}
  • Generators is a very big topic in javascript so if learn more about it you can learn Here. but for now, we have learned everything that is necessary to understand async/await.

    Async/Await :

  • for me perfect the async pattern in terms of readability is that it just looks like a normal synchronous but it pauses the function when we are executing async code.
  • javascript has run-to-completion semantic. which means that when js engine starts executing a function it will not pause/stop until that function is finished. because of this reason, it was not possible to pause/stop the function.
  • But from ES6 we got the generator functions which will be paused when it encounters yield and it resumes when we call next on the iterator created from the generator function.
  • let's take a pause and think about what would happen if we combine Promise and generator. i.e what if we can yield the response of the promise in the generator function. it may something look like this

perfectAsync(function*(){
   const user = yield fetch('/currentuser');
   const todos = yield fetch('/todos',{user:user});
   console.log(todos);
})

NOTE: this is not the correct javascript code but it is just our imagination.

  • now let's look at what this code trying to do. imagine we have a function called perfectAsync which takes the generator function as an argument and executes the function. whenever we yield the promise in this function, perfectAsync will unwrap the fulfillment response and it will assign to the variable so in this example user will be object, not the promise. and todos will be array.

  • inspiring from this pattern ES2017 introduce something called the async function. with this function, you can pause the function whenever you are executing something asynchronous and it resumes after executing asynchronous code.


async function getTodosOfUser() {
       const user = await fetch('/currentuser');
       const todos = await fetch('/todos',{user:user});
       console.log(todos); // array of todos.
}
  • here with help of the await keyword whenever we are executing the fetch request it will pause the function (NOTE: here it will pause the function but not the javascript thread. so your thread will be free to execute asynchronous and synchronous tasks) and whenever we get the response of promise it will unwrap the fulfillment value for us.

  • error handling async/await is also quite straightforward. you need to use try/catch just like synchronous code.


async function getTodosOfUser(){
   try{
        const user = await fetch('/currentuser');
        const todos = await fetch('/todos',{user:user});
        console.log(todos); // array of todos.
   }catch(e){
        console.error(e);
   }
}
  • async function returns the promise so that you can abstract your code easily.

async getUser(){
    const user = await fetch('/currentuser');
    return user;
} // returns promise

async function getTodosOfUser(){
   try{
        const user = await getUser(); // unwraps that promise.
        const todos = await fetch('/todos',{user:user});
        console.log(todos);
   }catch(e){
        console.error(e);
   }
}
  • Here, getUser() does not return the user object but it returns a promise containing the user object which will be unwarped by the await keyword in getTodoUser.because of this we always get the expected behavior. (if the async function would return the simple value, await keyword will not able to unwrap the response because it always expects the promise. because of this it always returns a promise).

  • As you can Async/await is just syntactic sugar around promise. so In your codebase, if you use promises, async/await will just fit in fine.

  • that's it, folks. I think now you know how async/await works under the hood in javascript and how they are just syntactic sugar around promise and generator; not some alien tech.