Expressive ES6 features that shine in Node.js 4.0

The developers behind Node.js released version 4.0 last week. The new version is a major update that includes significant improvements and many new language features. It is the first release to incorporate improvements from io.js, a community-driven fork that recently reintegrated with upstream Node.js.

The io.js project emerged earlier this year when prominent Node.js contributors decided to go their own way, seeking more open governance and a more predictable release cycle. They recently resolved their differences with Joyent, the corporate steward of the Node.js project. Following the reconciliation, the upstream maintainers adopted the io.js code base.

Node.js 4.0 includes the latest and greatest version of the V8 JavaScript runtime, which opens the door for developers to use a number of new language features from the ECMAScript 6 (ES6) specification. In previous versions of Node, important ES6 features were only available while using experimental command line flags. Some of our favorite new features supported out of the box in Node.js 4.x include arrow functions, generators, for/of loops, and template strings.

At RethinkDB, we’re pretty excited to start using those new language features in our Node.js demo applications. As you will see, the new syntax works seamlessly with our existing RethinkDB client driver for Node.js.

Generators and yield

We highlighted the power of generators earlier this year when we took io.js for a spin and demonstrated how developers can use generators to simplify asynchronous programming. When an asynchronous task is expressed with a generator, you can make it so that the yield keyword suspends execution of the current method and resumes when the desired operation is complete. Much like the C# programming language’s await keyword, it flattens out asynchronous code and allows it to be written in a more conventional, synchronous style.

Third-party coroutine libraries let you use the yield keyword to achieve await-like behavior for any expression that returns a JavaScript Promise. That means that you can use yield for asynchronous programming with existing libraries, such as the RethinkDB client driver. Instead of nested callback spaghetti or complex Promise chains with anonymous functions, you get cleaner and more maintainable code. Consider the following example:

Node.js 0.12:

r.connect().then(function(conn) {
  return r.table("fellowship").coerceTo("array").run(conn)
    .finally(function() { conn.close(); })
})
.then(function(data) {
  console.log(data);
});

Node.js 4.0 with ES6:

bluebird.coroutine(function*() {
  var conn = yield r.connect();
  var data = yield r.table("fellowship").coerceTo("array").run(conn);

  console.log(data);
  conn.close();
})();

Using yield for asynchronous operations also simplifies exception handling. You can use it with conventional try/catch blocks instead of chaining a catch method.

Arrow functions

Arrow functions provide a more concise syntax for expressing anonymous functions. They implicitly return their value and inherit this from the parent scope. Using arrow functions instead of the more verbose anonymous function syntax can make your ReQL queries cleaner and more readable:

Node.js 0.12:

r.connect().then(function(conn) {
  return r.table("posts")
    .map(function(doc) { return doc("comments").count(); })
    .reduce(function(left, right) { return left.add(right); })
    .default(0).run(conn).finally(function() { conn.close(); });
})
.then(function(data) {
  console.log(data);
});

Node.js 4.0 with ES6:

bluebird.coroutine(function*() {
  var conn = yield r.connect();
  var data = yield r.table("posts")
    .map(doc => doc("comments").count())
    .reduce((left, right) => left.add(right))
    .default(0).run(conn);

  console.log(data);
  conn.close();
})();

Looping with for and of

When you iterate over an array in JavaScript with the conventional for/in syntax, you get the index of each iteration rather than the value. That typically increases the complexity of looping and requires you to keep the array around in a separate variable. Node.js 4 introduces support for the new for/of loop syntax, which lets you iterate over the actual values of the sequence. That can be useful when you iterate over the return value of a RethinkDB query:

Node.js 0.12:

r.connect().then(function(conn) {
  return r.expr([1,2,3])
    .map(function(x) { return x.mul(2); })
    .run(conn).finally(function() { conn.close(); });
})
.then(function(data) {
  for (var i in data)
    console.log(data[i]);
});

Node.js 4.0 with ES6:

bluebird.coroutine(function*() {
  var conn = yield r.connect();

  for (var i of yield r.expr([1,2,3]).map(x => x.mul(2)).run(conn))
    console.log(i);

  conn.close();
})();

In the example above, I used yield inside of the for expression so that loop starts when the asynchronous query is complete.

Template strings

Template strings provide straightforward support for string interpolation. Anywhere inside of a template string, you can include an arbitrary JavaScript expression inside of a ${} wrapper. The runtime will evaluate the expressions and include their output within the string at the designated position. Template strings are much cleaner than performing conventional string concatenation with the + operator. Template strings can also span multiple lines, which is often convenient if you want to generate HTML output. In RethinkDB applications, template strings make it easy to generate strings that incorporate the output of a query:

Node.js 0.12:

r.connect().then(function(conn) {
  return r.table("quake")
    .orderBy(r.desc(r.row("properties")("mag")))("properties")
    .limit(10).run(conn).finally(function() { conn.close(); });
})
.then(function(data) {
  for (var quake in data)
    console.log("There was a " + data[quake].mag + "magnitude earthquake" + data[quake].place);

Node.js 4.0 with ES6:

bluebird.coroutine(function*() {
  var conn = yield r.connect();

  var query = r.table("quake")
    .orderBy(r.desc(r.row("properties")("mag")))("properties")
    .limit(10);

  for (var quake of yield query.run(conn))
    console.log(`There was a ${quake.mag} magnitude earthquake ${quake.place}`)

  conn.close();
})();

Next steps

If you’d like to build a Web application with Node.js 4 and RethinkDB, consider using Koa, a next-generation Node.js web framework designed by the team behind Express. Koa is very similar to Express, but it makes extensive use of generators to provide a cleaner way of integrating middleware components. When you use Koa, every route handler acts like a coroutine: you can use yield inside for asynchronous operations.

The following example demonstrates how to build a simple API backend with an endpoint that returns data from a query:

Node.js 0.12 with Express:

var app = require("express")();
var r = require("rethinkdb");

app.get("/fellowship/species/:species", function(req, res) {
  r.connect().then(function(conn) {
    return r.table("fellowship")
            .filter({species: req.params.species})
            .coerceTo("array").run(conn)
        .finally(function() { conn.close(); });
  })
  .then(function(output) { res.json(output); });
});

app.listen(8090);

Node.js 4.0 with ES6 and Koa:

var app = require("koa")();
var route = require("koa-route");
var r = require("rethinkdb");

app.use(route.get("/fellowship/species/:species", function *(species) {
  var conn = yield r.connect();
  this.body = yield r.table("fellowship")
                     .filter({species: species})
                     .coerceTo("array").run(conn);
  conn.close();
}));

app.listen(8090);

By bringing the latest stable V8 features to the masses, the latest Node.js release delivers some compelling syntactic improvements. These features are no longer experimental–you can take advantage of them in your applications today.

Give RethinkDB a try with Node 4. You can follow our thirty-second RethinkDB quickstart guide.

Resources