The Hidden Power of ES6 Generators: Observable Async Flow Control
But in that article, I intentionally sidestepped another major use-case for generators. Arguably, the primary use case: Asynchronous flow control.
Async / Await
It did not make it into ES6. It will not make it into ES2016. It could become standard in ES2017, and then we’ll need to wait for all the JS engine implementations to land before we can use it. (Note: it works in Babel now, but that’s no guarantee. Tail call optimization worked in Babel for several months but got subsequently removed).
In spite of the wait, you’ll still find a bunch of articles talking about async/await. Why?
It can turn code like this:
Into code like this:
Notice that in the first version, our promise-based function has an extra layer of nesting. The async/await version looks like regular, synchronous code, but it’s not. It yields the promise and exits the function, freeing the JS engine to do other things, and when the promise from `fetchSomething()` resolves, the function resumes, and the resolved promise value is assigned to `result`.
What I’d like to take a deeper look at is how async / await might use generators under the hood… and how you can use them for synchronous style flow control right now, today, without waiting for async / await to arrive.
Generator functions are a new feature in ES6 that allow a function to generate many values over time by returning an object which can be iterated over… an iterable with a `.next()` method that returns objects like this:
The `done` property indicates whether or not the generator has yielded its last value.
Talking Back to Generators
Here’s where things get really fun. Communication with generators can happen in both directions. In addition to receiving values from generators, you can inject values into the generator function. The iterator `.next()` method can take values to be assigned.
There are a couple other ways to communicate to generators. You can throw errors at them. Instead of calling next, you can call `iter.throw(error)`, for example, to communicate that something went wrong fetching data for the generator. You can also force the generator to return with `iter.return()`.
Both of those might come in handy to add error handling to flow control code.
Generators + Promises = The Holy Grail
What if there was a function wrapping that generator that could detect when you yield a promise, wait for it to resolve, and then pass the resolved value back into the generator with the subsequent `.next()` call?
Then you could write async/await style code like this:
It turns out that a library like that already exists. It’s called Co.js. But instead of teaching you how to use Co, let’s try to figure out how we could write something like that ourselves. Looking at the `crossBridge()` example above, it looks like it should be pretty easy.
We’ll start with a simple `isPromise()` function:
Next, we’ll need a way to iterate through the generator’s `.next()` calls, unwrap the promises, and wait for them to resolve before calling `.next()` again. Here’s a straightforward approach with no error handling. This is just a demonstration of the idea. You don’t want to use this in production — your errors would get swallowed, and it would be very hard to debug what’s going on:
As you can see, we’re passing in a callback to return the final value. We communicate with the generator by passing the previous value into the `.next()` call at the top of the function. That’s what allows us to assign the result of the previous `yield` call to identifier:
Of course, none of this works until you kick it all off — and what about the promise that actually returns the final value?
Let’s take a look at all of it together… the whole thing is about 22 lines of code, excluding the usage example:
Now, if you want to start using this technique in your code, definitely use Co.js, instead. It has the error handling you’ll need (which I only skipped to avoid cluttering the example), it’s production tested, and it has a couple other nice features.
From Promises to Observables
The example above is interesting, and Co.js is indeed useful to simplify asynchronous flow control. There’s just one problem: It returns a promise. As you’re probably aware, a promise can only emit a single value or rejection…
Initially, I was very excited about generators, but now that I’ve been living with them for a while, I haven’t found a lot of good use cases for generators in my real application code. For most use-cases I might use generators for, I reach for RxJS instead because of its much richer API.
Because (unlike a generator function) a promise can only emit one value, and (like a generator function) an observable can emit many, I personally believe that the observable API is a much better fit for async functions than a promise.
What’s an observable?
The table above is from the GTOR: A General Theory of Reactivity, by Kris Kowal. It breaks things down neatly across space & time. Values that can be pulled synchronously consume space (values in memory), but are detached from time. They are pull APIs.
When dealing with future values, you need to be notified when a value becomes available. That’s the push.
A promise is a push mechanism that calls some code after the promise has been resolved or rejected with a single value.
An observable is like a promise, but it calls some code every time a new value becomes available, and can emit many values over time.
The core feature of an observable is a `.subscribe()` method which takes three values:
- onNext — Called each time the observable emits a value.
- onError — Called when the observable encounters an error or fails to generate the data to emit. After an error, no further values will be emitted, and `onCompleted` will not be called.
- onCompleted — Called after it has called `onNext` for the final time, but only if no errors were encountered.
So, if we want to implement an observable API for our synchronous-style async functions, we just need a way to pass in those parameters. Let’s take a crack at that, leaving `onError` for later:
I really like this version, because it feels a lot more versatile to me. In fact, I like it so much, I’ve fleshed it out a bit, renamed it to Ogen, added error handling and a true Rx Observable object (which means you can `.map()`, `.filter()` and `.skip()` to your heart’s content. Among other things.
Check out Ogen on GitHub.
There are lots of ways observables can improve your asynchronous flow control, which is probably the main reason I haven’t used generators a lot more, but now that I can mix and match synchronous-style code and observables seamlessly with Ogen, maybe I’ll start to use generators a whole lot more.
He spends most of his time in the San Francisco Bay Area with the most beautiful woman in the world.