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.