A Gentle Introduction to Node.js Promises with Q: Part 1

1 comment


Note: I use the terms "Node.js", "Node", and "JavaScript" somewhat interchangeably throughout this post.

Introduction

I love Node.js. My first encounter with it was in 2012 while working in IBM Research. We used it, along with (at the time) some other recent and hip technologies, to build a web app. I loved how terse and flexible Node was, compared with Java. And I loved how JavaScript doesn't quite know what kind of a language it is (prototype-based? object-oriented? functional?).

Node.js can be described as JavaScript for the backend. But as anyone who has programmed anything serious in the language knows, this is not completely accurate. What really makes writing JavaScript for the backend different than writing JavaScript for the web browser is that, on the backend, nearly everything is asynchronous, non-blocking, and callback-based. For instance, to read a file, you would write something like:
fs.readFile('myfile.txt', 'utf8', function(err, contents) {
  if (err) {
    console.error('error: ' + err.message);
  } else {
    console.log(contents);
  }
});


Sometimes Node provides synchronous versions of asynchronous calls, but those are for the weak. A real man or woman embraces Node's asynchronous nature. But it isn't always easy, especially if you're first starting out. One of the first annoyances a new Node.js coder encounters is the infamous "pyramid of doom" that results from nested callbacks:
asyncFunc1(arg1, function(err, result) {
  if (err) {
    // Handle error
  }
  asyncFunc2(arg2, function(err, result) {
    if (err) {
      // Handle error
    }
    asyncFunc3(arg3, function(err, result) {
      if (err) {
        // Handle error
      }
        // Etc.
    });
  });
});

Before too long, you're indenting lines to half the width of your screen. Not only that, but you're having to write a million error handlers, most of which probably do the same thing, for every asynchronous call. And no one likes writing error handlers.

One oft-cited solution for this predicament is to create named callback functions at global scope. This works, but it isn't ideal. The most annoying thing about this solution is that you have to come up with names for all of these continuation functions. What do you call such functions? "everythingAfterReadingTheFirstFile"? "everythingAfterRequestingTheWebPage"? Additionally, these functions, declared at global scope, lose the benefit of closures.

There is a much better solution, and it is called a promise. Promises are neither a new concept in programming nor are they specific to Node, but I had never used them before. There are several promise libraries out there, but one of the most popular -- and the one I use -- is called "Q". Like its Star Trek counterpart by the same name, "Q" is powerful and annoying. But it is only annoying if you don't understand it. I'm here to make it your friend.

Using Q

Everything I'm about to tell you can be learned from the Q documentaton. But like man pages, the Q documentation can be a bit overwhelming, especially for someone who hasn't yet adjusted to its paradigm. So I'm going to walk you through the very basics of using Q in this first blog post. Subsequent posts will expand upon this one, until we've completely mastered the Q library and you're using it like a pro.

A promise is an object that represents the future return value -- or the thrown exception -- of an asynchronous call. There are two operations you need to know to use promises. The first is how to create a promise. The second is how to tell that promise what to do once it becomes "fulfilled" or "rejected". A promise is fulfilled when its asynchronous calls eventually return a value. A promise is rejected when an error is thrown.

Creating a Promise

There are many ways to create a promise. Here is the simplest (and least useful):
var promise = Q();
promise eventually becomes fulfilled with the value undefined. Not particularly useful. It is slightly more useful to pass in a value:

var promise = Q(7);


promise now eventually becomes fulfilled with the value 7. Apart from passing values to initialize a promise, you can also pass synchronous functions to be executed to the method fcall:

var promise = Q.fcall(function() {
  return 7;
});

Here, we turn a synchronous function into an asynchronous function that returns the value 7. At this point, you're wondering why you're even reading this blog post, as this seems completely useless. All right. Let's see a useful example. Let's see how we make a promise with fs.readFile.

fs.readFile is an asynchronous function that returns nothing (undefined) and takes a callback. We can make a new function that mimics fs.readFile, but does *not* take a callback, and instead returns a promise. This is how we do that:

var readFileQ = Q.denodeify(fs.readFile);

Q.denodeify takes in a function that expects a callback of the usual Node.js form (err, result) and produces a new function that takes no callback, and instead returns a promise. So we can now write:

var promise = readFileQ('myfile.txt', 'utf8');

When that line is executed, promise does not contain the contents of the file. After all, readFileQ is an asynchronous call. It instead contains an object that, at some point in the future, will contain the contents of the file (or an exception, if myfile.txt does not exist). So how do we get at those precious file contents? Do we wait around for a few seconds, perhaps with a setTimeout call? NO! Read on.

Telling a promise what to do once it becomes fulfilled or rejected

To tell a promise what to do once it becomes fulfilled, you pass in a fulfillment handler to the promise method then:

promise.then(function(contents) {
  console.log(contents);
});
To tell a promise what to do in the unfortunate circumstance that it is rejected, you pass in an error handler as the second argument to then:

promise.then(function(contents) {
  console.log(contents);
}, function(err) {
  console.error('error: ' + err.message);
});

What does the method then return? Another promise! So, you could have written:

promise = promise.then(function(contents) {
  console.log(contents);
}, function(err) {
  console.error('error: ' + err.message);
});
Now, promise represents the return value of either its fulfillment handler or rejection handler, depending on which was actually called. But neither handler actually returned a value. They both just outputted some message, so promise eventually represents the value undefined. While the handlers could have returned a value like 7, it would have been even more powerful if they'd returned another promise! In that case, promise would have "become" that new promise, representing its eventual return value. This is best illustrated with an example:

var promise = readFileQ('myfile.txt', 'utf8')
.then(function(myFileContents) {
    console.log(myFileContents);
    return readFileQ('anotherfile.txt', 'utf8');
}, function(err) {
    console.error('error: ' + err.message);
})
.then(function(anotherFileContents) {
    console.log(anotherFileContents);
}, function(err) {
    console.error('error: ' + err.message);
});

Now, if all went well, the program would have read in two files and outputted their contents. But there are two annoyances here. First, we had to write the same error handler twice -- once for the first call to readFileQ, and again for the second call to readFileQ. Couldn't we just write one error handler? And second, what if, after the first readFileQ call fails, we don't want to try to read the second file? The answer to both problems is another promise method, called fail:

readFileQ('myfile.txt', 'utf8')
.then(function(myFileContents) {
  console.log(myFileContents);
  return readFileQ('anotherfile.txt', 'utf8');
})
.then(function(anotherFileContents) {
  console.log(anotherFileContents);
})
.fail(function(err) {
  console.error('error: ' + err.message);
});

This code snippet illustrates how we can attach one error handler to the end of the "promise chain" that will be called in the event that either the original call to readFileQ or any of the handlers throws an exception. If an error occurs at any point in the chain, the rest of the chain is skipped, and the error handler passed to fail is executed.

Hopefully you see what I mean by the "promise chain". Promises can be chained together, using the method then (and other methods I'll discuss in subsequent posts). The result of such a chain is one big promise, that becomes fulfilled when the last of the asynchronous functions returns.

There is a lot more I have to say on the subject of promises and Q. I've shown you just a fraction of what can be done with them, and I've only just started to explain their countless benefits. But hopefully this wets your whistle for more.

For the next part of this tutorial, I'll go into more detail about how promises work, and explain how to do things like make a promise for multiple asynchronous calls executed in parallel. Stay tuned!

-Todd

1 comment :

  1. This is a well-explained tutorial to follow.

    Thank you.

    Hung

    ReplyDelete