Promise + Callback pattern for JavaScript

As I've been working with Promises more, I've found a need to migrate existing code from callback passing syntax to returned promise syntax. Since the Node ecosystem favors the callback passing syntax, it is wise to make your library support both callbacks and Promises.

Fortunately, making your functions support both is fairly easy. Using Q, you can manually make your function support both styles. The first example uses Q's Deferreds and manually implements the logic that is required to support both techniques.

function AsyncFunc(next) {  
  var deferred = new Q.defer();  
  db.findOne({}, function(err, doc) {
    if(err) {
      deferred.reject(err);
      if(next) return next(err);
    }     
    else {
      deferred.resolve(doc);
      if(next) return next(null, doc);
    }     
  });  
  return deferred.promise;
}

The code above creates a Deferred object. When the async call completes, its rejects or resolves the promise appropriately. It also checks to see if there is a callback via the next argument. It will call the callback with the appropriate values for success or error states.

All in all, this is verbose and you end up with a fair amount of boilerplate. We can do better.

Fortunately, Q has a built-in function called nodeify that will do some work for us! Here's how the same function looks using nodeify

function AsyncFunc(next) {  
  var deferred = new Q.defer();  
  db.findOne({}, function(err, doc) {
     if(err) deferred.reject(err);
     else deferred.resolve(doc);     
  });  
  return deferred.promise.nodeify(next);
}

That certainly is a bit cleaner. The nodeify method is attached to the Promise prototype. It will take care of calling the node-style callback if it exists.

Q also supports ES6-style Promise syntax instead of Deferred synxtax. In my opinion this is a cleaner syntax. It also has the benefit of fewer key-strokes.

function AsyncFunc(next) {  
  return Q.Promise(function(resolve, reject) {
    db.findOne({}, function(err, doc) {
      if(err) reject(err);
      else resolve(doc);
    });   
  })
  .nodeify(next);
}

Lastly, if you want to support similar behavior in ES6 promises, you can use this polyfill. The code is similar to that found in Q and can be used in the same manner.

Promise.prototype.nodeify = function (nodeback) {  
  if (nodeback) {
    this.then(function (value) {
      setTimeout(function () {
        nodeback(null, value);
      }, 0);
    }, function (error) {
      setTimeout(function () {
        nodeback(error);
      }, 0);
    });
  } else {
    return this;
  }
};
function AsyncFunc(next) {  
  return new Promise(function(resolve, reject) {
    db.findOne({}, function(err, doc) {
      if(err) reject(err);
      else resolve(doc);
    });   
  })
  .nodeify(next);
}
comments powered by Disqus