Combining Promises and Generators to Write Elegant Asynchronous Code in Javascript
Introduction
The purpose of this blog post is to teach you how promises and generators can be combined to write asynchronous code in Javascript that looks synchronous. Writing code this way makes it very easy to read and reason about what the code is doing. I’m not going to cover the topic of promises because there are plenty of resources online that provide much better explanations than I could. I will, however, provide a brief overview of generators in Javascript, but I recommend that you pursue additional reading elsewhere.
Generators
Generators were added to Javascript as a result of ES6. A generator in JavaScript is a special type of function that can halt itself at any time using the keyword yield, and then later resume execution. Whats so interesting about generators is that they allow developers to write functions that not only can “return” multiple times, but can also have values “injected” into them mid-execution.
If that sounded confusing, don’t worry, generators quickly begin to make sense once you start playing around with them. That said, open up your favorite ES6-compliant* Javascript REPL and paste in the following code:
1 /*the asterisk indicates that the function is a
2 generator instead of a traditional function */
3 var generatorFactory = function* () {
4 var result1 = yield 1;
5 console.log(result1);
6 var result2 = yield 2;
7 console.log(result2);
8 var result3 = yield 3;
9 console.log(result3);
10 }
11
12 var testGenerator = new generatorFactory();
Now try calling testGenerator.next() a few times. You should get something that looks like this:
1 console.log(testGenerator.next());
2 // Object {value: 1, done: false}
3 console.log(testGenerator.next());
4 // undefined
5 // Object {value: 2, done: false}
6 console.log(testGenerator.next());
7 // undefined
8 // Object {value: 3, done: false}
9 console.log(testGenerator.next());
10 // undefined
11 // Object {value: undefined, done: true}
Everytime you call testGenerator.next(), the function executes until it encounters a yield
statement. The yield
statement functions like return
in that the invoking function will evaluate to whatever value directly follows the keyword yield
(except it will be wrapped in a container object, but we’ll get to that in a moment). The first time you call testGenerator.next()
, the generatorFactory
function will execute until it encounters the first yield
statement:
var result1 = yield 1;
At this point the generator will yield (pausing execution) and the statement testGenerator.next()
will evaluate to an object that contains two properties:
- value: the yielded value
- done: a boolean value that represents whether or not the generator has any more code to execute (false in this case)
The generator will remain paused until the next time testGenerator.next()
is called. This process repeats itself everytime we call testGenerator.next()
until there are no more yield
statements, at which point the done property will be true.
But why did all the console logs evaluate to undefined? When .next()
is called, the yield statement is replaced with whatever parameter is passed to .next()
. This is how values can be “injected” into generators mid-execution. To see this in action, create a new testGenerator
and then execute the following code:
1 console.log(testGenerator.next());
2 // Object {value: 1, done: false}
3 console.log(testGenerator.next(4));
4 // 4
5 // Object {value: 2, done: false}
6 console.log(testGenerator.next(5));
7 // 5
8 // Object {value: 3, done: false}
9 console.log(testGenerator.next(6));
10 // 6
11 // Object {value: undefined, done: true}
The values that are passed into .next()
as parameters are stored in the result
variables, and subsequently console logged.
Combining Generators and Promises
Ok, now that we have a basic understanding of promises and generators, how can we combine them to write elegant and easy-to-understand asychronous code? Well first, lets take a look at the problem we’re trying to solve:
1 var ajaxFunction = function(val) {
2 promisifiedAjaxCall(val).then(function(result1) {
3 anotherPromisifiedAjaxCall().then(function(result2) {
4 if (result1 === someValue && result2 === someOtherValue) {
5 doSomeThing();
6 }
7 else {
8 doSomeOtherThing();
9 }
10 });
11 });
12 });
13 ajaxRoutine(val);
Even with only two asynchronous calls, this function is starting to get a little messy, and we’re not even handling errors yet! Lets try rewriting this function using a combination of promises and a generators:
1 var ajaxGenerator = function*(val) {
2 var result1 = yield promisifiedAjaxCall(val);
3 var result2 = yield anotherPromisifiedAjaxCall();
4 if (result1 === someValue && result2 === someOtherValue) {
5 doSomeThing();
6 }
7 else {
8 doSomeOtherThing();
9 }
10 }
11 var testGenerator = new ajaxGenerator(val);
12 var result1Promise = testGenerator.next().value.then(function(result1) {
13 var result2Promise = testGenerator.next(result1).value.then(function(result2) {
14 testGenerator.next(result2);
15 });
16 });
So whats going on here? Lets walk through how this code will execute. The first time testGenerator.next()
is called on line 12, ajaxGenerator
will run until it hits the first yield
statement on line 2.
var result1 = yield promisifiedAjaxCall(val);
result1Promise
variable will then be assigned a promise. Once the ajax call is complete and the promise resolves, the .then()
function will execute, and the response from the ajax call will be passed back into the ajaxGenerator
function (as a result of invoking .next(result1)
). Inside of the generator, the response from the ajax call will be assigned to result1
.
var result1Promise = testGenerator.next().value.then(function(result1) {
var result2Promise = testGenerator.next(result1).value.then(function(result2) {
testGenerator.next(result2);
});
});
The generator function will resume execution until it hits the yield
statement on line 3 at which point the process will repeat, eventually assigning the result of the second ajax call to result2
. The code can now be thought of as looking like this:
1 var ajaxRoutine = function*(val) {
2 var result1 = resultFromFirstAjaxCall;
3 var result2 = resultFromSecondAjaxCall;
4 if (result1 === someValue && result2 === someOtherValue) {
5 doSomeThing();
6 }
7 else {
8 doSomeOtherThing();
9 }
10 }
Now that the ajax calls have been resolved, its very easy to reason about the remaining code in linear fashion as you would with any synchronous functon. This is nice, but invoking the generator and feeding the resolved promises back into it is a messy process. In fact, because we’re using nested callbacks to manage the invocations of testGenerator.next()
, we haven’t really solved the problem at all yet. The whole point was to stop using nested callbacks in the first place!
What we want is a function that not only yields promises, but can automatically wait for them to resolve, feed their values back into itself, and then resume execution. Luckily, the Bluebird promise library has such a function. The code snippet above can be rewritten like this:
1 var Promise = require('bluebird');
2
3 var ajaxRoutine = Promise.coroutine(function*(val) {
4 var result1 = yield promisifiedAjaxCall();
5 var result2 = yield anotherPromisifiedAjaxCall();
6 if (result1 === someValue && result2 === someOtherValue) {
7 doSomeThing();
8 }
9 else {
10 doSomeOtherThing();
11 }
12 });
13
14 ajaxRoutine(val).catch(function(err) {
15 if (err) throw err;
16 });
This time, the same process will occur and the code will execute asynchronously, waiting for each ajax request to complete before continuing, but the code can be read and reasoned about in a synchronous manner. Pretty dope, huh?
Note that since Promise.coroutine()
returns a promise, any errors that occur within the coroutine function can be caught by appending a single .catch()
to the function invocation. With a single line of code, we’ve error handled the whole thing!
1 var Promise = require('bluebird');
2
3 var ajaxRoutine = Promise.coroutine(function*(val) {
4 var result1 = yield promisifiedAjaxCall();
5 if (result1 === someValueIDontLike) return null;
6 var result2 = yield anotherPromisifiedAjaxCall();
7 if (result1 === someValue && result2 === someOtherValue) {
8 doSomeThing();
9 }
10 else {
11 doSomeOtherThing();
12 }
13 });
14
15 /* ------ capturing return value ------ */
16 var returnedVal = ajaxRoutine(val).catch(function(err) {
17 if (err) throw err;
18 });
But wait, there’s more! Promise.coroutine
actually returns a promise, so we can chain .then
onto the coroutine invocation, like so:
1 var Promise = require('bluebird');
2
3 var ajaxRoutine = Promise.coroutine(function*(val) {
4 var result1 = yield promisifiedAjaxCall();
5 if (result1 === someValueIDontLike) return null;
6 var result2 = yield anotherPromisifiedAjaxCall();
7 if (result1 === someValue && result2 === someOtherValue) {
8 doSomeThing();
9 }
10 else {
11 doSomeOtherThing();
12 }
13 });
14
15 /* ------ capturing return value ------ */
16 var returnedVal = ajaxRoutine(val).then(function() {
17 console.log('This wont happen until the coroutine is done executing!');
18 }).catch(function(err) {
19 if (err) throw err;
20 });
Even more interestingly, since a generator is a function, we can actually use return
statements! The only requirement is that we return a promise:
1 var Promise = require('bluebird');
2
3 var ajaxRoutine = Promise.coroutine(function*(val) {
4 var result1 = yield promisifiedAjaxCall();
5 if (result1 === someValueIDontLike) return null;
6 var result2 = yield anotherPromisifiedAjaxCall();
7 if (result1 === someValue && result2 === someOtherValue) {
8 doSomeThing();
9 return somePromise();
10 }
11 else {
12 doSomeOtherThing();
13 return someOtherPromise();
14 }
15 });
16
17 /* ------ capturing return value ------ */
18 var returnedVal = ajaxRoutine(val).then(function(resolvedPromiseValue) {
19 console.log('This is the resolved value from the promise: ', resolvedPromiseValue);
20 }).catch(function(err) {
21 if (err) throw err;
22 });
Hopefully now you understand how promises and generators can be combined to write elegant, easy-to-read asynchronous code. Spread the word! Next time someone complains to you about how they don’t like Node.js because ‘callback hell is unavoidable’, tell them about generators and promises!
*Note: If you want to use generators in Node, you need to have version 0.12 or higher AND run node with the –harmony option enabled. For example: node –harmony server.js
Alternatively, you can use io.js which natively supports ES6 functionality.