We use cookies on our website, hope you don’t mind. Just imagine a plate of delicious meringues, they look like clouds. Read more

Development

ES6 Generators – Is This the End of Callback Hell in JS?

ES6 Generators - Is This the End of Callback Hell in JS?

Oncoming ECMAScript6 standards bring many exciting possibilities to JavaScript. One of these are generators, known well for Python, Ruby, or C# developers. On the web you can find many articles about this subject. Particularly interesting, especially for node.js programmers, are the opportunities to flatten asynchronous code. Unfortunately, with this potential, there is also a lot of confusion and problems understanding the topic. In this article I will explain the basics of generators, show their practical use, and, as the main goal, explain their capabilities in flattening asynchronous code. The reader is expected to have basic knowledge of asynchronous code in JavaScript and at least some experience with Promises pattern.

To run the examples, you will need an environment that supports generators. At the time of writing this article, the obvious solution seems to be node.js in 0.12 version with the –harmony flag enabled.

Generators – What Are They All About?

A generator is a function whose execution can be stopped at any time and resumed later. Similar solutions have been available in many languages for a long time, as I mentioned in the introduction. How does it look in JavaScript?

/**
 * generator control code
 */
var numbersGenerator, iteratorResult;

// create generator. at this point it's paused
numbersGenerator = numbersGenFn();

// give up control to generator
iteratorResult = numbersGenerator.next();
// print generator returned value and state 
console.log(iteratorResult.value, iteratorResult.done); // 1, false

// give control back to generator...
iteratorResult = numbersGenerator.next();
console.log(iteratorResult.value, iteratorResult.done); // 2, false

// give control back to generator and send some value to it
iteratorResult = numbersGenerator.next(10);
console.log(iteratorResult.value, iteratorResult.done); // 13, false

iteratorResult = numbersGenerator.next();
console.log(iteratorResult.value, iteratorResult.done); // undefined, true

/**
 * generator definition
 */
function* numbersGenFn() {
    // pause code execution and return 1 to control code
    yield 1;

    // pause code execution, return 2 to control code
    var x = yield 2;
    // when code is resume, store send data in x variable

    // pause code execution, return sum of x and 3
    yield x + 3;
}

numbersGenerator.next(val) gives control to the generator and optionally sends to it some value. The keyword yield does the opposite, giving control to the main code and returning a value to it. Most important in understanding the above code is realizing the order of the program sequence. The picture below should be helpful in this. For the implementation details, check the attached materials.

ES6 Generators - Is This the End of Callback Hell in JS?

In Practice…

In the beginning, the idea can seem quite complicated. In what way can it actually be useful? I’ll use the language Python , which has supported generators for a long time as an example. Each of you probably use a standard for loop on a daily basis. It can be found in similar form in many languages, including JavaScript :

for (var i = 0; i < 10; i++) {
  ...
}

Python uses, according to many people, a simpler form:

for i in range(10):
  pass

Achieving similar construction in JavaScript should not be difficult. Therefore, let’s try:

function range(count) {
  var numbers = [];
  for (var i = 0; i < count; i++) {
    numbers.push(i);
  }
  return numbers;
}

// we use ES6 `of` feature, which iterates over any iterable (e.g. array, generators)
for (var i of range(10)) {
  ...
}

Although simple, our solution unfortunately has some drawbacks that can easily escape our attention. The problem is the memory when you try to iterate through very large numbers.

var count = 100000000;
for (var i of range(count)) {
  ...
}

In my case, only 100 million was enough to get the error: process out of memory . The reason is the numbers array, which has to be prepared before starting our for-of loop. It becomes simply too large, and it is unnecessary, because at the specific iteration moment, we need only one number. What if we used generators to solve this same problem?

function* xrange(count) {
    for (var nb = 0; nb < count; nb++) {
        yield nb++;
    }
}

for (var i of xrange(count)) {
  ...
}

As you can see, the code is even simpler. We do not need a local array (which was the source of our problems); we return one value at a time. Moreover, we can use the generator in same way as the usual array: we can iterate over it values using the for-of loop!

Flattening Code that Uses Promise

This is the most popular way to use generators. Many people confuse them with this particular solution, but this is just one of the many possibilities they give us.

We’ll show a simplified example of how to flatten your code using Promise mechanism, using the native form of Promise , node.js as well as most leading web browsers providing support for this standard. Let’s start by defining a simple Promise example: a promise that will be fulfilled after the specified number of milliseconds.

function delayedPromise(timeMs) {
    return new Promise(function (resolve, reject) {
        setTimeout(resolve, timeMs);
    });
};

function main() {
    delayPromise(400).then(function () {
      console.log('some time later');
      return delayPromise(600);
    }).then(function () {
      // 400 + 600
      console.log('1 sec later');
      return delayPromise(1000);
    }).then(function () {
      // 400 + 600 + 1000 = 2 sec after start
      console.log('Done');
    });
}

Well, it certainly looks better than using native setTimeout and multiple nested callbacks. Unfortunately, we have still a lot of repetitive code. How much better would it look if we could convince our program to simply wait a specified period of time? (Of course, we do not want to suspend the entire application and make our dear user angry.)

function* mainGenerator() {
    yield delayPromise(400);
    console.log('some time later');
    yield delayPromise(600);
    console.log('1 sec later');
    yield delayPromise(1000);
    console.log('done');
}

We can quite easily teach our program to use the Promise in this way to make the above code work. We need a helper function that, by using generators, handles the entire mechanism for us.

function coroutine(generatorFn) {
    // create suspended generator object
    var generator = generatorFn();
    // run our handler method
    onFulfilled();

    function onFulfilled() {
        // give control to generator method
        var next = generator.next();
        // check if generator is done
        if (!next.done) {
            // we expect than we got promise from generator.
            // we wait if is fulfilled and call same `onFulfilled` again -
            // in this way we give control back to generator
            next.value.then(onFulfilled);
        }
    }
}

Look closely at the next.value.then(onFulfilled) line. Here we have what might be called an asynchronous recursion. The function does not call itself directly, but indicates that it has to be re-called after fulfilling the Promise . This is a simplified model; however, you do not need more to run our mainGenerator() example. To start it, we simply pass the previously prepared generator to our helper function.

coroutine(mainGenerator);

And that’s enough. Our asynchronous but quite readable and flat code executes correctly. As you can see, there is no magic behind it – just a few lines of code and the power of generators.

Unfortunately, our example is not perfect. We make many assumptions. It does not check whether you really yield a Promise . We ignore the value returned from the generator and do not send the fulfilled Promise value. And the most important – lack of errors handling. It’s a good start, but we need to go a little farther.

Promise Rejections Handling

To keep it simple, we assume that the values we pass to yield are actually instances of Promise . Let’s start with two simple promises. delayPromiseProvide() is a Promise of a specific value after passing a certain amount of milliseconds. delayPromiseReject() waits a certain number of milliseconds and rejects with a specified error.

function delayPromiseProvide(timeMs, val) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            resolve(val);
        }, timeMs);
    });
}

function delayPromiseReject(timeMs, err) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            reject(err)
        }, timeMs);
    });
}

function* mainGenerator() {
    // just wait 400ms
    yield delayPromiseProvide(400);
    console.log('some time later');
    try {
        // wait another 600ms and reject this promise
        yield delayPromiseReject(600, new Error('smth went wrong'));
    } catch (err) {
        // rejected promises are throwing like regular exceptions 
        console.log('catched error - 1 sec later', err);
    }
    // after another second we will got 'test value' in `val` variable
    var val = yield delayPromiseProvide(1000, 'test value');
    console.log('done -', val);

    // at the end just return regular variable
    return 'simple text';
}

coroutine(mainGenerator)
    .then(function (result) {
        console.log('all is fine:', result);
    });

Expected output of calling coroutine(mainGenerator) are progressive printing texts:

some time later
catched error - 1 sec later [Error: smth went wrong]
done - test value
all is fine: simple text

This gives us more opportunities. First, our coroutine function returns an instance of the Promise object. We’ll also handle errors and return the result of yielded promises to generator variable ( var val = yield … ). Unfortunately, at this moment our coroutine function is not ready for these tasks. So let’s move forward.

function coroutine(generatorFn) {
    //create Generator object
    var generator = generatorFn();

    try {
        return onFulfilled();
    } catch (err) {
        // if something went wrong, return rejected promise
        return Promise.reject(err);
    }

    function handleNext(next) {
        if (next.done) {
            // if we are at the end, just return promisified value and end work
            return Promise.resolve(next.value);
        }
        // attach success and failure callbacks for the promise
        return next.value
            .then(onFulfilled)
            .catch(onRejected);
    }

    function onFulfilled(val) {
        // send `val` to generator, default `undefined`
        var next = generator.next(val);
        return handleNext(next);
    }

    function onRejected(err) {
        // throw error "inside" generator function, so it can be handling by it
        var next = generator.throw(err);
        return handleNext(next);
    }
}

If you understood the first case, this one should not be much more difficult. What is new is the line var next = generator.throw(err) . This code will throw an exception “inside” the generator code, which can also be caught and handled there. It then returns the same object as generator.next() , with the value obtained from the next yield in the generator function order.

I hope this knowledge of the basics of generators and using them with asynchronous code will prove useful for you.

More…

Generators on MDN

Promises on MDN

ECMAScript 6 Features

Flattening Callbacks Using Promises

Wiktor Obrębski

Software Engineer

you may also like these posts

Blog image

This React Native Feature Saves Your Company Time and Money

Blog image

Design is Vital for Front-End Developers: How to Learn It?

Blog image

Is Flutter The Best Way To Build Cross-Platform Mobile Apps?

Free project quote

Fill out the enquiry form and we'll get back to you as soon as possible.


Thank you for your message!


We’ll get back to you in 24 hours.

Meanwhile take a look at our blog.

Read our blog
Gareth N. Genner Photograph

Gareth N. Genner

Co-Founder of Trust Stamp

Quote

We needed a partner who could take on our idea, and make it real. 10Clouds bring so many different skills. We feel that every member that’s involved in the project is a member of our team.