Why go asynchronous
Asynchronous programming is a great paradigm which offers a key benefit over its synchronous counterpart – non blocking I/O within a single threaded environment. This is achieved by allowing I/O operations such as network requests and reading files from disk to run outside of the normal flow of the program. By doing so this enables responsive user interfaces and highly performant code.
The challenges faced
To people coming from a synchronous language like PHP, the concept of asynchronous programming can seem both foreign and confusing at first, which is understandable. One moment you were programming one line at a time in a nice sequential fashion, the next thing you know you’re skipping entire chunks of code, only to jump back up to those chunks at some time later. Goto anyone? Ok, it’s not that bad.
Then, you have the small matter of callback hell, a name given to the mess you can find yourself in when you have asynchronous callbacks nested within asynchronous callbacks several times deep – before you know it all hell has broken loose.
Promises came along to do away with callback hell, but for all the good they did, they still did not address the issue of code not being readable in a nice sequential fashion.
Generators in ES6
With the advent of ES6, along came a seemingly unrelated paradigm – generators. Generators are a powerful construct, allowing a function to “yield” control along with an (optional) value back to the calling code, which can in turn resume the generator function, passing an (optional) value back in. This process can be repeated indefinitely.
Consider the following function, which is a generator function (note the special syntax), and look at how its called:
function *someGenerator() {
console.log(5); // 5
const someVal = yield 7.5;
console.log(someVal); // 10
const result = yield someVal * 2;
console.log(result); // 30
}
const it = someGenerator();
const firstResult = it.next();
console.log(firstResult.value); // 7.5
const secondResult = it.next(10);
console.log(secondResult.value); // 20
result.next(30);
Can you see what’s going on? The first thing to note is that when a generator is called, an iterator is returned. An iterator is an object that knows how to access items from a collection, one item at a time, keeping track of where it is in the collection. From there, we call next on the iterator, passing control over to the generator, and running code up until the first yield statement. At this point, the yielded value is passed to the calling code, along with control. We then call next, passing in a value and with it we pass control back to the generator function. This value is assigned to the variable someVal
within the generator. This process of passing values in and out of the generator continues, with console log’s providing a clearer picture of what’s going on.
One thing to note is the de-structuring of value
from the result of each call to next
on the iterator. This is because the iterator returns an object, containing two key value pairs, done
, and value
. done
represents whether the iterator is complete. value
contains the result of the yield statement.
Using generators with promises
This mechanism of passing control out of the generator, then at some time later resuming control should sound familiar – that’s because this is not so different from the way promises work. We call some code, then at some time later we resume control within a thenable block, with the promise result passed in.
It therefore only seems reasonable that we should be able to combine these two paradigms in some way, to provide a promise mechanism that reads synchronously, and we can!
Implementing a full library to do this is beyond the scope of this article, however the basic concepts are:
- Write a library function that takes one argument (a generator function)
- Within the provided generator function, each time a promise is encountered, it should be yielded (to the library function)
- The library function manages the promise fulfillment, and depending on whether it was resolved or rejected passes control and the result back into the generator function using either
next
orthrow
- Yielded promises should be wrapped in a try catch
For a full working example, check out a bare bones library I wrote earlier in the year called awaiting-async, complete with unit tests providing example scenarios.
How this looks
Using a library such as this (there are plenty of them out there), we can take the following code from this:
const somePromise = Promise.resolve('some value');
somePromise
.then(res => {
console.log(res); // some value
})
.catch(err => {
// (Error handling code would go in here)
});
To this:
const aa = require('awaiting-async');
aa(function *() {
const somePromise = Promise.resolve('some value');
try {
const result = yield somePromise;
console.log(result); // some value
} catch (err) {
// (Error handling code would go in here)
}
});
And with it, we’ve made asynchronous code look synchronous in JavaScript!
tl;dr
Generator functions can be used in ES6 to make asynchronous code look synchronous.