Write "Synchronous" Node.js Code with ES6 Generators

Nov 21, 2015

One of the most amazing things about ES6 (EcmaScript 2015) is the introduction of generator functions. These are special functions that may be paused at any time as they wait for an async operation to complete by utilizing the yield expression, and are resumed as soon as that operation completes. Their context (variable bindings and so forth) are saved across function re-entrances.

function* getAccounts() {
    var accounts = yield db.Account.find({});
    console.log(accounts); // This code will only run once the previous statement completes!
}

Generator functions enable us to write asynchronous code in a synchronous manner. For an in-depth explanation on how ES6 generators work under the hood, check out this great explanation by StrongLoop.

Callback Hell

Generators are great when used correctly in Node.js. Since most of your Node code runs single-threaded, you must avoid blocking the main thread with slow IO operations, such as querying the DB, requesting a web page, etc. One way to do this is with callbacks, which are JavaScript functions that are executed only when the underlying operation completes. If you've had a chance to work with Node, you're probably familiar with what many refer to as callback hell:

// POST /login
exports.login = function login(req, res) {
    var email = req.body.email;
    var password = req.body.password;

    security.rateLimitRequest( 'reset', req, function( err ) {
        if ( err )
            return res.status( 400 ).send( err );
        db.Account.find({email: email, password: password}, function(err, account) {
            if ( err )
                return res.status( 400 ).send( err );
            account.getAccountStatistics(account, function(err, account) {
                if ( err )
                    return res.status( 400 ).send( err );
                account.incrementAccountLoginCount(account, function(err) {
                    if ( err )
                       return res.status( 400 ).send( err );
                    res.send(account);
                });
            });
        });
    });
});

Notice the pyramid structure of the code. There are a few problems that arise when using nested callbacks:

  1. The code is extremely ugly and hard to maintain and will eventually create a horizontal scrollbar in your editor.
  2. The error handling logic needs to be applied to each callback - lots of duplicate code.
  3. It's really hard to modularize this code and split it into separate files and functions.

ES6 Generators to the Rescue!

Generators allow a function to be suspended and resumed via the yield keyword. Generator functions have a special syntax: function* (). They make it possible to write code that looks "synchronous", therefore greatly simplifying your code logic and avoiding callback hell.

Here's how the above code would look after utilizing ES6 generators:

// POST /login
exports.login = function *login() {
    var email = req.body.email;
    var password = req.body.password;

    try
    {
        // Throws an error if rate limit exceeded
        yield security.rateLimitRequest( 'reset', req );

        // Query MongoDB for account
        var account = yield db.Account.find({email: email, password: password});
        account.statistics = yield account.getAccountStatistics(account);

        // Increment login count
        yield account.incrementAccountLoginCount(account);

        this.body = account;
    }
    catch( err ) {
        // Return the error as JSON
        return res.status( 400 ).send( err );
    }
});

Notice the try-catch syntax as well -- when a generator function throws an exception, execution of the current function will stop (the statements after throw won't be executed), and control will be passed to the first catch block in the call stack, thereby avoiding implementation of an error handler for each async operation!

Using Generators in a Node.js Web API

Now that you know what generators are, and how to use them, let's go ahead and write a basic proof-of-concept Node.js REST API that queries data from MongoDB with ES6 generators.

Note that we'll need a local MongoDB instance and an up-to-date version of Node.js which supports generators out of the box. I'd recommend installing v4.2.2 from here.

First, open a terminal and create a directory for our project:

mkdir generator-example
cd generator-example
npm init

The developers behind the popular Express framework released a brand-new web framework called Koa which they refer to as the next generation web framework:

Koa is a new web framework designed by the team behind Express, which aims to be a smaller, more expressive, and more robust foundation for web applications and APIs. Through leveraging generators Koa allows you to ditch callbacks and greatly increase error-handling. Koa does not bundle any middleware within core, and provides an elegant suite of methods that make writing servers fast and enjoyable.

Let's go ahead and install it, along with mongoose, the oh-so-popular MongoDB ODM:

npm install koa mongoose --save

(the --save parameter saves the dependencies to the package.json we created earlier)

Create an index.js in your favorite Node.js editor (mine's Visual Studio Code) with the following code:

// Dependencies
var koa = require('koa');
var mongoose  = require('mongoose');

// Set up MongoDB connection
var connection = mongoose.connect('localhost/test');

// Define an example schema
var Bear = mongoose.model( 'bears', new mongoose.Schema({
    name:           String,
    description:    String
}));

// Create koa app
var app = koa();

// Koa middleware
app.use(function *(){
    // Create a new bear
    var bear = new Bear();

    bear.name = "Great White Bear";
    bear.description = "A wonderful creature.";

    // Save the bear
    yield bear.save();

    // Query for all bears
    var bears = yield Bear.find({});

    // Set bears as JSON response
    this.body = bears;
});

// Define configurable port
var port = process.env.PORT || 3000;

// Listen for connections
app.listen(port);

// Log port
console.log('Server listening on port ' + port);

Save the file, start MongoDB and run the app with the following commands:

mongod
npm start

Navigate your browser to http://localhost:3000, and lo-and-behold, you have just written your first Node.js + MongoDB REST API without a single callback! Refresh the page multiple times, and you'll notice that the list of bears grows with each refresh.

Other Alternatives to Callbacks

There are other alternatives to nested callbacks in Node.js, such as promises and async. However, ES6 generators win hands-down in my opinion, for their simplicity, their try-catch syntax support, and because they're built into the language.

A Complete Example

Want to add more routes to your API? Check out this complete Koa + Mongoose REST API example project I put together on GitHub (based off of Elzair/koa-mongodb-example).