Faking Co-Routines, or: Why Callback Hell Is Over

Generators are now in the latest developer versions of the Chrome and Firefox browsers and Node.js. Which means you can approximate coroutines. The biggest hole in the most widely used programming language in history just got fixed.

“Callback hell” does not matter any more.

The Biggest Hole In JavaScript

A generator is a special function that can interact with its caller during its execution. A better name for generators might be cooperating functions. In fact, generators are a form of coroutines, which is a fancy name for cooperating functions. Coroutines allow you to define code with multiple entry points for suspending and resuming execution. The most widely-used example of coroutines is Unix pipes.

Coroutines are one of several approaches to the problem of concurrency, multi-tasking. Coroutines are a form of cooperative multi-tasking, meaning that we build multi-tasking directly into the code we write. Pre-emptive multi-tasking means that our code doesn’t know about any other tasks besides its own.

Threads are a form of pre-emptive_multi-tasking, while coroutines, fibers, and generators are forms of cooperative multi-tasking. You’ll find one or more of these features in Ruby, Python, Java, and Go, among other languages. You can also implement coroutine support in any language which supports continuations. However, until ES6, JavaScript relied entirely on callbacks for cooperative multitasking.

Concurrency With Generators

The idea behind coroutines is that multiple functions can cooperate by yielding control to one another. JavaScript’s generators are more limited, in that they can only pass control back to the caller. Still, this turns out to be enough, because we can introduce a coordinator function to pass control from the caller to any function we choose.

This cooperation, in turn, allows us to do multiple tasks concurrently. Thus, coroutines are closely related to JavaScript’s asynchronous programming model. And, obviously, it’s no accident that generators, which are a limited form of coroutine, are part of the new JavaScript standard. We can use generators to pass control back to the caller once an asynchronous function returns, so that the code for asynchronous operations can all be in the same function.

On a simple, pragmatic level, this means Node.js developers no longer need to deal at all with the phenomenon known as callback hell. One of the major pitfalls of Node development just turned into a non-issue.

Callback Hell

Until now, JavaScript developers had only one language-level mechanism to manage co-operative multitasking: callbacks. You can implement interesting abstractions on top of callbacks, and, of course, people have. Stockholm syndrome set in for others. Still others added low-level multitasking primitives, or even introduced new language constructs.

Free at Last

JavaScript still has its problems, but lack of language support for concurrent programming is no longer one of them. Since I haven’t seen too many simple examples that get right to the point on this, I’ll just include one here, using CoffeeScript, which has generators in HEAD.

Let’s look at a simple example: converting a file from Markdown to HTML. Here’s a synchronous version:

Of course, what we typically want is the asynchronous version. Here’s an implementation using callbacks.

Of course, there are already alternatives to using callbacks. We could use streams, for example. But streams are an awkward fit here, because we need to read in the entire file in order to parse it. We’d have to write a faux stream to fit the parsing step into a pipeline.

We could use promises or event emitters or any number of other abstractions, too, of course. But, with ES6, we can just do this:

The key lines here are line 8 and line 10 which pass control back to the caller (the call function, which is basically a controller function). Control is returned back to us when the asynchronous calls complete. The use of try/catch is also interesting, because we no longer lose exceptions that happen in another thread of execution. Generally, all the logic for our conversion now resides within the same function, instead of being split across callbacks.

The ES6 version looks a lot like the synchronous version. Thanks to generators and promises, we’re back in a world where functions return values and errors can be propagated back up the stack as needed. Yet we still get the benefit of non-blocking code!

Caveats

I’ve hidden some important details in the example to avoid confusion. Behind the scenes, what’s happening in the second listing is considerably more complex than in the first. That will cause some trouble when debugging. There’s an ES7 proposal which could move the implementation into the JavaScript runtime, which would help.

Still, Though…

Faking coroutines with generators and promises isn’t any more complex than any of the myriad solutions we already have for this problem. And it’s based entirely on the ES6 standard, so you don’t have to worry about being opinionated, which was a problem with using a solution like node-fibers—you’d force everyone who used your library to use node-fibers, too. But generators and promises are standards now.

Example: Building HTTP API Clients With Shred

Let’s consider a more interesting example. Shred is our library for building HTTP clients. Supply it with a CSON file defining the HTTP requests in an API, and it will automatically construct a client for that API.

The newest version uses fake coroutines. Here’s a simple client for the GitHub Issues API:

And here’s how we can get a list of issues for a project:

The first yield is in response to the call to the API. The second is a bit more interesting, because we’re don’t appear to be making a call. But one thing that’s nice about the way this works is that we can yield on any promise.

Handling the Response Body

In this case, the promise is the data property of the API response. That’s a promise that’s fulfilled when the response is fully streamed to the client. In other words, yield data replaces this:

Of course, this isn’t always what you want. One of the great things about Node is that streams are the default API. Shred gives us both a streamable response and a data property we can yield to when that’s all we need.

Example: Asynchronous Testing With Amen

Testing an asynchronous library like Shred is tricky because the tests have to wait for the responses to return. We wrote testify, a simple functional testing library, to help make that easier. With the availability of generators, we introduced amen, which makes asynchronous testing even easier.

Here’s a test for Shred in Amen:

If we give Amen a generator function, it yields, resuming when we get a response. Our test itself uses yield and ends with a simple assert. If the assert throws, the test fails. In other words, it’s pretty much the same as a synchronous test in terms of how we write it.

But…I Don’t Like Magic

How does this really work behind the scenes? It’s actually quite clever, which is why the stack trace can be a little confusing if you get an error. When a generator function yields, it can return a value. That includes promises, of course.

So if we wrap a generator function in a controller function that waits for the yielded promise to resolve before resuming (via the generator’s next) function, we can iterate through each asynchronous call until the function exits.

What you see on the stack trace, then, is the controller managing this iteration. Of course, you can skim past this, but it does tend to obscure what’s going on in your functions.

More Complex Asynchronous Patterns

What about dealing with more complex scenarios, like a bunch of parallel calls, where you want to return when they’re all finished? Remember, with this technique, you can yield to any thing returns a promise, so this is straightforward. In fact, most promise libraries provide a way to do this already, so you can just use the library’s existing functions.

We use when.js. Here’s how we might make a bunch of parallel HTTP calls:

Now we can simply yield to parallel_get with a list of URLs and get back an array of responses once all the GET requests complete.

Using Generators With Node and CoffeeScript

If you’re familiar at all with generators, you may have noticed the lack of any special function syntax in any of the above examples. That’s because CoffeeScript is very smart about generators—if a function has a yield statement, it must be a generator function.

Using generators in Node is pretty straightforward.

  1. First, install nvm:

(You may want to add that last line to your shell startup script.)

  1. Next, install the latest Node 0.11.x:
  1. And then just run your Node scripts with the --harmony flag:
node --harmony my-script.js

To use CoffeeScript takes a tiny bit of extra work because you have to install CoffeeScript from the development branch:

Now when you run CoffeeScript, you’ll get the generator-aware version. But you still need to tell Node to use the --harmony flag:

coffee --nodejs --harmony my-script.coffee

And you’re good to go!