Refusing Callbacks: Generators in ECMAScript 6

Original author: Erin Swenson-Healey
  • Transfer
I constantly hear people whining about asynchronous callbacks at JavaScript. Keeping in mind the order of execution in this language is a little difficult (this is the case called the “ Callback Hell ” or “ The Pyramid of Doom ”), if you have previously dealt with synchronous programming. My usual answer was "you have to deal with it somehow." After all, do we expect all programming languages ​​to look and feel the same? Of course not.

Everything was changed by the recent draft review ECMAScript 6, which describes generators - the possibility of a language that will completely change our way of writing both server and client JavaScript. Using generators, we can turn nested callbacks into synchronous code without blocking our onlyevent loop.
For example, this code:
    setTimeout(function(){
        _get("/something.ajax?greeting", function(err, greeting) {
            if (err) { console.log(err); throw err; }
            _get("/else.ajax?who&greeting="+greeting, function(err, who) {
                if (err) { console.log(err); throw err; }
                console.log(greeting+" "+who);
            });
        });
    }, 1000);

can be written like this:
    sync(function* (resume) {
        try (e) {
            yield setTimeout(resume, 1000);
            var greeting = yield _get('/something.ajax?greeting', resume)
            var who = yield _get('/else.ajax?who&greeting=' + greeting, resume)
            console.log(greeting + ' ' + who)
        }
        catch (e) {
            console.log(e);
            throw e;  
        } 
    });

Interesting, isn't it? Centralized exception handling and clear execution order.


Uh, ECMAScript 6?


The examples in this article will work in Chrome Canary 33.0.1716.0. Examples, with the exception of those where available XHR, should work in Node.jswith the flag --harmony(from version 0.11, approx. Transl. ). The generator implementation proposed in JavaScript 1.7+does not adhere to the draft ECMAScript 6- so you will have to make some changes to make the examples work in Firefox. If you want to run these examples in Canary, you can run them in the same form as here.

ES6 generators


In order to understand what is happening in the examples above, we need to talk about what ES6generators are and what they allow you to do.

According to the draft ECMAScript 6, generators are “first-class coroutines, which are objects that encapsulate pending execution contexts”. Simply put, generators are functions that can stop their execution (using a keyword yield) and continue their execution from the same place after calling their method next.JavaScriptstill performs only one task at a time, but he is now able to pause the execution of the generator function in the middle of the body and switch the context to the execution of something else. Generators do not allow parallel code execution and they do not know how to handle streams.

Modest iterator


Now that we’ve figured out a bit, let's look at the code. We will write a small iterator to demonstrate the stop / continue syntax.
    function* fibonacci() {
        var a = 0, b = 1, c = 0;
        while (true) {
            yield a;
            c = a;
            a = b;
            b = c + b;
        }
    }
    function run() {
        var seq = fibonacci();
        console.log(seq.next().value); // 0
        console.log(seq.next().value); // 1
        console.log(seq.next().value); // 1
        console.log(seq.next().value); // 2
        console.log(seq.next().value); // 3
        console.log(seq.next().value); // 5
    }
    run();

What's going on here:
  1. The function runinitializes the Fibonacci number generator (it is described by special syntax funсtion*). Unlike a regular function, this call does not start executing its body, but returns a new object - a generator.
  2. When a function runcalls the generator method next(synchronous operation), the code is executed until it encounters an operator yield.
  3. Executing the statement yieldstops the generator and returns the result out. The operations following yieldat this moment were not executed. The value (operand abehind yield) will be accessible externally through the property valueof the execution result.
    The next time the method is called nextby the generator, the execution of the code continues from where it stopped at the previous one yield.


You are probably wondering if the generator will ever go out of the loop while. No, it will execute inside the loop until someone calls its method next.

Track code execution


As shown in the previous example, the code located in the body of the generator after yieldwill not be executed until the generator is continued. It is also possible to pass an argument to the generator, which will be substituted instead of the yieldone on which the previous execution of the generator was interrupted.
    function* powGenerator() {
        var result = Math.pow(yield "a", yield "b");
        return result;
    }
    var g = powGenerator();
    console.log(g.next().value);   // "a", from the first yield
    console.log(g.next(10).value); // "b", from the second
    console.log(g.next(2).value);  // 100, the result

The first run of the generator returns a value "a"as a property of the valueresult of the run. Then we continue the execution by passing the value to the generator 10. We will use substitution to demonstrate what happens:
    function* powGenerator() {
        var result = Math.pow(----10----, yield "b");
        return result;
    }

Then the generator reaches the second yieldand again pauses its execution. The value "b"will be available in the returned object. Finally, we continue execution again, passing as an argument 2. Substitution again:
    function* powGenerator() {
        var result = Math.pow(----10----, ----2----);
        return result;
    }

After that, the method is called pow, and the generator returns the value stored in the variable result.

Fake synchronization: blocking Ajax


The Fibonacci sequence iterator and math functions with many entry points are interesting, but I promised to show you a way to get rid of callbacks in your JavaScriptcode. As it turns out, we can take some ideas from the previous examples.

Before we look at the following example, pay attention to the function sync. She creates a generator by passing a function to it resumeand calls a method nexton it to start its execution. When a generator needs an asynchronous call, it uses it resumeas a callback and executes yield. When an asynchronous call completes resume, it calls the method next, continuing the execution of the generator and passing the result of the asynchronous call to it.

Back to code:
    // **************
    // framework code
    function sync(gen) {
        var iterable, resume;
        resume = function(err, retVal) {
            if (err) iterable.raise(err);
            iterable.next(retVal); // resume!  
        };
        iterable = gen(resume);
        iterable.next();
    }
    function _get(url, callback) {
        var x = new XMLHttpRequest();
            x.onreadystatechange = function() {
                if (x.readyState == 4) {
                callback(null, x.responseText);
            }
        };
        x.open("GET", url);
        x.send();
    }
    // ****************
    // application code
    sync(function* (resume) {
        log('foo');
        var resp = yield _get("blix.txt", resume); // suspend!
        log(resp);
    });
    log('bar'); // not part of our generator function’s body

Can you guess what you will see in the console? The correct answer is “foo”, “bar” and “what is in blix.txt”. By positioning the code inside the generator, we make it look like regular synchronous code. We do not block the flow event loop; we stop the generator and continue to execute the code located further after the call next. The future callback, which will be called on another tick, will continue our generator, passing it the desired value.

Centralized error handling


Centralized error handling inside multiple asynchronous callbacks is a pain. Here is an example:
    try {
        firstAsync(function(err, a) {
            if (err) { console.log(err); throw err; }
            secondAsync(function(err, b) {
                if (err) { console.log(err); throw err; }
                thirdAsync(function(err, c) {
                    if (err) { console.log(err); throw err; }
                    callback(a, b, c);
                });
            });
        });
    }
    catch (e) {
        console.log(e);
    }

A block catchwill never be executed due to the fact that callback execution is part of a completely different call stack, in a different tick event loop. Exception handling should be located inside the callback function itself. You can implement a higher order function to get rid of some repeated checks for errors and remove some attachments using a library like async. If you follow Node.jsthe error convention as the first argument, you can write a general handler that will return all errors back to the generator:
    function sync(gen) {
        var iterable, resume;
        resume = function(err, retVal) {
            if (err) iterable.raise(err); // raise!
            iterable.next(retVal);
        };
        iterable = gen(resume);
        iterable.next();
    }
    sync(function* (resume) {
        try {
            var x = firstAsync(resume);
            var y = secondAsync(resume);
            var z = thirdAsync(resume);
            // … do something with your data
        }
        catch (e) {
            console.log(e); // will catch errors from any of the three calls
        }
    });

Now, the exceptions that occur inside any of the three functions will be handled by a single block catch. And the exception that occurred in any of the three functions will not allow subsequent functions to be executed. Very well.

Simultaneous operations.


The fact that the generator code is executed from top to bottom does not mean that you cannot work with several asynchronous operations at the same time. Libraries are kind gennyand gen-rungive such an API: they simply perform a certain number of asynchronous operations before continuing to run the generator. Example using genny:
    genny.run(function* (resume) {
        _get("test1.txt", resume());
        _get("test2.txt", resume());
        var res1 = yield resume, res2 = yield resume; // step 1
        var res3 = yield _get("test3.txt", resume()); // step 2
        console.log(res1 + res2);
    });

Total


Asynchronous callbacks have been the de facto primary pattern JavaScriptfor a long time. But now, along with the generators in the browser ( Firefoxfrom JavaScript 1.7and a Chrome Canaryfew months ago), everything is changing. New execution control constructs make it possible to use a completely new programming style, one that can compete with the traditional style of nested callbacks. It remains to wait for the standard to ECMAScript 6be implemented in tomorrow's engines JavaScript.

Also popular now: