
Refusing Callbacks: Generators in ECMAScript 6
- Transfer
I constantly hear people whining about asynchronous callbacks at
Everything was changed by the recent draft review
For example, this code:
can be written like this:
Interesting, isn't it? Centralized exception handling and clear execution order.
The examples in this article will work in
In order to understand what is happening in the examples above, we need to talk about what
According to the draft
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.
What's going on here:
You are probably wondering if the generator will ever go out of the loop
As shown in the previous example, the code located in the body of the generator after
The first run of the generator returns a value
Then the generator reaches the second
After that, the method is called
Fake synchronization: blocking
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
Before we look at the following example, pay attention to the function
Back to code:
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
Centralized error handling inside multiple asynchronous callbacks is a pain. Here is an example:
A block
Now, the exceptions that occur inside any of the three functions will be handled by a single block
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
Asynchronous callbacks have been the de facto primary pattern
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.js
with 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
ES6
generators 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
.JavaScript
still 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:
- The function
run
initializes the Fibonacci number generator (it is described by special syntaxfunсtion*
). Unlike a regular function, this call does not start executing its body, but returns a new object - a generator. - When a function
run
calls the generator methodnext
(synchronous operation), the code is executed until it encounters an operatoryield
. - Executing the statement
yield
stops the generator and returns the result out. The operations followingyield
at this moment were not executed. The value (operanda
behindyield
) will be accessible externally through the propertyvalue
of the execution result.
The next time the method is callednext
by the generator, the execution of the code continues from where it stopped at the previous oneyield
.
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
yield
will 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 yield
one 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 value
result 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
yield
and 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
JavaScript
code. 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 resume
and calls a method next
on it to start its execution. When a generator needs an asynchronous call, it uses it resume
as 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
catch
will 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.js
the 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
genny
and gen-run
give 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
JavaScript
for a long time. But now, along with the generators in the browser ( Firefox
from JavaScript 1.7
and a Chrome Canary
few 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 6
be implemented in tomorrow's engines JavaScript
.