Exploring JavaScript Generators
- Transfer
- Tutorial

When I started writing on node.js, I hated two things: all the popular templating engines and a huge number of callbacks. I voluntarily used callbacks because I understood the full power of event-oriented servers, but since then generators have appeared in JavaScript , and I look forward to the day when they will be implemented.
And now this day is coming. Today, generators are available in V8 and SpiderMonkey, implementation follows specification updates - this is the dawn of a new era!
While V8 hides new Harmony features, such as generators, behind the command line flag, this will be the case for a while. Before they become available in all browsers, we can go ahead and learn how to write asynchronous code with generators. Let's try these approaches early on.
You can use them today by downloading the unstable version of node 0.11 , which will be the next stable version. When starting node, pass the
--harmonyor flag --harmony-generators. So how do you use generators to save you from callback hell? Generator functions can pause execution using an operator
yield, and pass the result in or out when they resume or pause. In this way, we can pause when a function waits for the result of another function without passing a callback to it. Isn't it fun when I try to explain language constructs in our language? How about diving into code?
Generator Basics
Let's look at a primitive generator before we dive into the asynchronous world. Generators are declared by the expression
function*:function* foo(x) {
yield x + 1;
var y = yield null;
return x + y;
}
The following is an example call:
var gen = foo(5);
gen.next(); // { value: 6, done: false }
gen.next(); // { value: null, done: false }
gen.send(8); // { value: 13, done: true }
If I took notes in the class, I would write:
yieldallowed in all expressions.- A generator call is identical to a regular function, but it creates a generator object. You need to call
nextorsendto resume the generator.sendused when you want to send a value back to it.gen.next()equivalent togen.send(null). So there isgen.throwone that throws an exception inside the generator. - The generator methods do not return a pure initial value, but an object with two parameters:
valueanddone. Thanks,doneit becomes clear when the generator is finished, either with or with thereturnsimple end of the function, instead of the inconvenient exceptionStopIterationthat was in the old API.
Asynchronous Solution # 1: Pause
What to do with the code in which callback hell? It’s good if we can arbitrarily pause the function. We can turn our asynchronous callback code back into synchronously-looking code with sugar crumbs.
Question: what is sugar?
The first solution is proposed in the suspend library . It is very simple. Only 16 lines of code , seriously.
This is how our code looks with this library:
var suspend = require('suspend'),
fs = require('fs');
suspend(function*(resume) {
var data = yield fs.readFile(__filename, 'utf8', resume);
if(data[0]) {
throw data[0];
}
console.log(data[1]);
})();
The function
suspendpasses your generator into the regular function that starts the generator. It passes the function resumeto the generator, the function resumeshould be used as a callback for all asynchronous calls, it resumes the generator with arguments containing the error and value flag. Dancing
resumeand the generator are interesting, but there are some disadvantages. Firstly, an array of two elements received back is inconvenient, even with destructuring ( var [err, res] = yield foo(resume)). I would like to return only the value, and throw an error as an exception, if any. In fact, the library supports this, but as an option, I think it should be the default.Secondly, it is inconvenient to always explicitly pass resume, moreover, it is unsuitable when you wait until the function above completes. And I still have to add
callbackand call it at the end of the function, as is usually done in node. Finally, you cannot use more complex threads of execution, for example with several concurrent calls. README claims that other flow control libraries are already solving this problem, and you should use it
suspendwith one of them, but I would rather see a flow control library that includes generator support. Author's supplement: kriskowal suggested this gist written by creationix, there is an improved stand-alone generator handler for callback-based code. It is very cool to throw errors by default.
Asynchronous Solution # 2: Promises
A more interesting way to control the asynchronous thread of execution is to use promises . Promise is an object that represents a future value, and you can provide promises to the calling thread of execution by a program that represents asynchronous behavior.
I will not explain the promises here, since it will take too much time and, besides, there is already a good explanation . Recently, emphasis has been placed on defining behavior and API promises for interaction between libraries, but the idea is quite simple.
I am going to use the Q library for promises, because it already has preliminary generator support, and is also quite mature. task.jswas an early implementation of this idea, but it had a non-standard implementation of promises.
Let's take a step back and look at a real life example. Too often we use simple examples. This code creates the message, then receives it back, and receives the message with the same tags (it
clientis an instance of redis):client.hmset('blog::post', {
date: '20130605',
title: 'g3n3rat0rs r0ck',
tags: 'js,node'
}, function(err, res) {
if(err) throw err;
client.hgetall('blog::post', function(err, post) {
if(err) throw err;
var tags = post.tags.split(',');
var posts = [];
tags.forEach(function(tag) {
client.hgetall('post::tag::' + tag, function(err, taggedPost) {
if(err) throw err;
posts.push(taggedPost);
if(posts.length == tags.length) {
// сделать что-то с post и taggedPosts
client.quit();
}
});
});
});
});
See how this example is ugly! Callbacks quickly push the code to the right side of our screen. In addition, to request all tags, we must manually manage each request and check when they are all ready.
Let's bring this code to Q promises.
var db = {
get: Q.nbind(client.get, client),
set: Q.nbind(client.set, client),
hmset: Q.nbind(client.hmset, client),
hgetall: Q.nbind(client.hgetall, client)
};
db.hmset('blog::post', {
date: '20130605',
title: 'g3n3rat0rs r0ck',
tags: 'js,node'
}).then(function() {
return db.hgetall('blog::post');
}).then(function(post) {
var tags = post.tags.split(',');
return Q.all(tags.map(function(tag) {
return db.hgetall('blog::tag::' + tag);
})).then(function(taggedPosts) {
// сделать что-то с post и taggedPosts
client.quit();
});
}).done();
We had to wrap redis functions, and thereby turned callback-based into promise-based, it's simple. As soon as we get promises, you call
thenand wait for the result of asynchronous operations. Much more details are explained in the promises / A + specification . Qimplements several additional methods, such as alltaking an array of promises and waiting for each of them to complete. In addition, there is doneone that says that your asynchronous process has completed and any unhandled errors should be thrown. According to the promises / A + specification, all exceptions should be converted to errors and passed to the error handler. This way you can be sure that all errors will be thrown if there is no handler on them. (If something is not clear, pleaseread this article from Dominic.) Notice how deep the final promise is. This is because we first need access to
post, and then to taggedPosts. Callback-style code is felt here, it is annoying. Now is the time to evaluate the power of generators:
Q.async(function*() {
yield db.hmset('blog::post', {
date: '20130605',
title: 'g3n3rat0rs r0ck',
tags: 'js,node'
});
var post = yield db.hgetall('blog::post');
var tags = post.tags.split(',');
var taggedPosts = yield Q.all(tags.map(function(tag) {
return db.hgetall('blog::tag::' + tag);
}));
// сделать что-то с post и taggedPosts
client.quit();
})().done();
Isn't that amazing? How does this actually happen?
Q.asynctakes a generator and returns a function that controls it, just like the suspend library. However, here the key difference is that the generator gives (yields) promises. Q accepts every promise and associates a generator with it, makes resume when the promise is fulfilled, and sends the result back. We don’t have to manage a clumsy function
resume- promises handles it completely, and we get the advantage of promises behavior . One of the advantages is that we can use different Q promises when necessary, for example
Q.allwhich runs several asynchronous operations in parallel. This way you can easily combine similar Q promises and implicit promises in generators to create complex execution threads that look very clean. Also note that we do not have a nesting problem at all. Since
postand taggedPostsare in the same scope, we no longer care about breakage in the scope chain then, which is incredibly pleasing. Error handling is very tricky, and you really need to understand how promises work before using them in generators. Errors and exceptions in promises are always passed to the error handling function, and they never throw exceptions.
Any
asyncthe generator is a promise, with no exceptions. You can manage errors using the callback error: someGenerator().then(null, function(err) { ... }). However, there is a special behavior of generators promises, which is that any errors from promises thrown into the generator using a special method
gen.throwwill be thrown except from the point where the generator was suspended. This means that you can use try/catchto handle errors in the generator:Q.async(function*() {
try {
var post = yield db.hgetall('blog::post');
var tags = post.tags.split(',');
var taggedPosts = yield Q.all(tags.map(function(tag) {
return db.hgetall('blog::tag::' + tag);
}));
// сделать что-то с post и taggedPosts
}
catch(e) {
console.log(e);
}
client.quit();
})();
This works exactly as you expect: errors from any call
db.hgetallwill be processed in the handler catch, even if it is an error in a deep promise inside Q.all. Without try/catchexception, it will be passed to the error handler of the caller's promise (if there is no caller, the error will be suppressed). Think about it - we can install exception handlers using try / catch for asynchronous code. The dynamic scope of the error handler will be correct; any unhandled errors that happen while the block
tryis running will be reported catch. You can use it finallyto create confident “cleanup” code at startup, even for errors, without the presence of an error handler. Also use
donealways when you use promises - this way you can get thrown errors by default instead of quiet ignoring, which too often happens with asynchronous code. The usage path Q.asyncusually looks like this:var getTaggedPosts = Q.async(function*() {
var post = yield db.hgetall('blog::post');
var tags = post.tags.split(',');
return Q.all(tags.map(function(tag) {
return db.hget('blog::tag::' + tag);
}));
});
Above is the library code that simply creates promises and does not handle error handling. You call it like this:
Q.async(function*() {
var tagged = yield getTaggedPosts();
// сделать что-то с массивом tagged
})().done();
This is the top level code. As mentioned earlier, the method is
doneguaranteed to throw an error for any unhandled error as an exception. I think this approach is common, but you need to call an extra method. getTaggedPostswill be used by promise-generating functions. The code above is simply top-level code that is filled with promises. I suggested Q.spawn in pull request , and these changes already hit Q! This makes it easy to run code that uses promises even easier:
Q.spawn(function*() {
var tagged = yield getTaggedPosts();
// сделать что-то с массивом tagged
});
spawnaccepts the generator, immediately starts it, and automatically forwards all unhandled errors. This is exactly equivalent Q.done(Q.async(function*() { ... })()).Other approaches
Our promised-based generator code begins to take shape. Together with grains of sugar, we can remove a lot of excess baggage associated with asynchronous workflow.
After some time working with generators, I highlighted several approaches.
Not worth it
If you have a short function that you need to wait for only one promise, it is not worth it to create a generator.
var getKey = Q.async(function*(key) {
var x = yield r.get(dbkey(key));
return x && parseInt(x, 10);
});
Use this code:
function getKey(key) {
return r.get(dbkey(key)).then(function(x) {
return x && parseInt(x, 10);
});
}
I think the latest version looks cleaner.
spawnMap
This is what I did often:
yield Q.all(keys.map(Q.async(function*(dateKey) {
var date = yield lookupDate(dateKey);
obj[date] = yield getPosts(date);
})));
It may be helpful to have
spawnMapone that performs Q.all(arr.map(Q.async(...)))for you.yield spawnMap(keys, function*(dateKey) {
var date = yield lookupDate(dateKey);
obj[date] = yield getPosts(date);
})));
This is similar to the method
mapfrom the async library .asyncCallback
The last thing I noticed: there are times when I want to create a
Q.asyncfunction and make it throw all the errors. This happens with normal callbacks from different libraries, such as express: app.get('/url', function() { ... }). I cannot convert the above callback to a
Q.asyncfunction, because then all errors will be quietly suppressed, I also can not use it Q.spawnbecause it is not executed immediately. Maybe something like asyncCallbackwould be good:function asyncCallback(gen) {
return function() {
return Q.async(gen).apply(null, arguments).done();
};
}
app.get('/project/:name', asyncCallback(function*(req, res) {
var counts = yield db.getCounts(req.params.name);
var post = yield db.recentPost();
res.render('project.html', { counts: counts,
post: post });
}));
As a summary
When I researched generators, I really hoped they would help with asynchronous code. And, as it turned out, they really do, although you need to understand how promises work in order to effectively combine them with generators. Creating promises makes implicit even more implicit, so I would not recommend that you use async or spawn until you understand the whole promise.
Now we have a concise and incredibly powerful way to code asynchronous behavior and we can use it for something more than just making operations for working with FS more beautiful. In fact, we have a great way to write concise, distributed code that can run on different processors, or even machines, while remaining synchronous.
Addendum from the author: read my next article,A look at generators without Promise .