ES6 Generators – Is This the End of Callback Hell in JS?
11.06.2015 | 5 min read
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?
def test_func(x): return x**2
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.
Python uses, according to many people, a simpler form:
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.
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?
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.
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.)
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.
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.
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.
Expected output of calling coroutine(mainGenerator) are progressive printing texts:
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.
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.