Co is a powerful tool for writing callback-free async code using vanilla ES2015. However, the sheer extent of co's utility is hard to understand unless you have a deep working knowledge of generators. With that in mind, here are 3 neat design patterns and tricks you can do with co that will help you grasp why co is so useful.

Retrying Failed Requests

Let's say you want to retry your HTTP requests a certain number of times if they fail. Co lets you retry requests using a plain-old for loop and try/catch. While this code looks synchronous, it's actually async, just co handles the async for you.

const co = require('co');
const superagent = require('superagent');

const NUM_RETRIES = 3;

co(function*() {
  let i;
  for (i = 0; i < NUM_RETRIES; ++i) {
    try {
      yield superagent.get('http://bad.domain');
      break;
    } catch(err) {}
  }
  console.log(i); // 3
});

You can even write a retry helper function using co.

const retry = (fn, numRetries) => co(function*() {
  for (let i = 0; i < numRetries; ++i) {
    try {
      yield fn();
      return;
    } catch(err) {}
  }
  throw new Error('ran out of retries');
});

const NUM_RETRIES = 3;

co(function*() {
  try {
    yield retry(() => superagent.get('http://bad.domain'), NUM_RETRIES);
  } catch(err) {
    console.log(err.toString()); // Error: ran out of retries
  }
});

Processing a Stream

Node.js streams enable you to process large data sets piece-by-piece. However, the primary way you interact with streams is using an EventEmitter API. Another interesting co design pattern is waiting for events - a promise can wrap any async operation, including waiting for an event.

Streams emit 3 events that you'll be concerned with in this example.

  • 'data' is emitted when the next chunk of data is ready
  • 'end' is emitted when the stream is done
  • 'error' is emitted when an error occurred.

Using the Promise.race() function, you can create a promise that resolves or rejects when the next event happens. Therefore, you can use co to process the stream using a while loop.

Here's an example that reads a file containing the text of Victor Hugo's Les Miserables and counts the number of times the main character's last name is mentioned in the text chunk by chunk.

const co = require('co');
const fs = require('fs');

const stream = fs.createReadStream('./les_miserables.txt');
let valjeanCount = 0;

co(function*() {
  while(true) {
    const res = yield Promise.race([
      new Promise(resolve => stream.once('data', resolve)),
      new Promise(resolve => stream.once('end', resolve)),
      new Promise((resolve, reject) => stream.once('error', reject))
    ]);
    if (!res) {
      break;
    }
    stream.removeAllListeners('data');
    stream.removeAllListeners('end');
    stream.removeAllListeners('error');
    valjeanCount += (res.toString().match(/valjean/ig) || []).length;
  }
  console.log('count:', valjeanCount); // count: 1120
});

Processing a MongoDB Cursor

MongoDB's find() function returns a cursor, which is an object that lets you load data from MongoDB in several different ways. You can use the toArray() function to get all the documents that match your query at once, you can convert it to a stream, or you can use the next() function to get the next document manually. Since next() returns a promise, you can use co to exhaust a MongoDB cursor using a plain old for loop.

const co = require('co');
const mongodb = require('mongodb');

co(function*() {
  const db = yield mongodb.MongoClient.connect('mongodb://localhost:27017');
  const cursor = db.collection('test').find();
  for (let doc = yield cursor.next(); doc != null; doc = yield cursor.next()) {
    console.log(doc); // log the docs one at a time
  }
});

Conclusion

Co is powerful because it enables you to write async code using common synchronous primitives, like for loops and try/catch. It enables you to deal with async primitives like streams and cursors with loops rather than callbacks, without sacrificing the async nature of JavaScript.

Want to know why the code examples in this article don't block the event loop, despite the fact that they look fully synchronous? Check out chapter 2 of my book, The 80/20 Guide to ES2015 Generators, which teaches you how to write your own co implementation. I also wrote a blog post about writing your own co.

Found a typo or error? Open up a pull request! This post is available as markdown on Github
comments powered by Disqus