Promises vs Callbacks - Code comparison

Promises vs Callbacks – Code comparison

I am not going to highlight pros of promises and cons of callbacks. There is plenty of good reading about this topic out there. I was recently writing simple Node module and decided to learn promises during its implementation. The result  can be interesting as comparison of Promises vs Callbacks approach applied to the same problems, because project contains

These are glued with Gulp build system to execute tests with all possible combinations. So for example Callbacks based test is executed also against Promises based module.

I named the project Jasstor. I am planning to use it for storage credentials into JSON file, hashing and verification of credentials. It stores user name, hashed password and user’s role in string format. Here is Github branch dedicated to this blog post, so that it’ll stay consistent. Please bear in mind, that I am learning Node development, ES6 features, Promises and Gulp on this project. So I could easily miss handy tricks or misused some features.

Project uses these main technologies:

I decided to have these constraints for Promises

  • Mocha will be excluded from promisification, so describe and it will be used with callbacks.
  • Jasstor‘s API will follow standard Node JS patterns
    • All functions are asynchronous with callback as last parameter
    • First parameter of callback is always error

When function signatures follow Node JS patters, it allows for promisification of modules. Such modules can be integrated into promise chain easily. But at the same time API isn’t tied to Promises at all. I like this approach because both camps (Promises or Callbacks fans) are happy.

Let’s take a look at code. I will explain and compare only most verbose use case and leave the rest for curios readers. You can find the code here.

Callback vs Promises – Tests comparison

Enough talking, let’s take a look at code. I’ll start with tests explanation as it promotes TDD thinking. Use case should test if existing password is overwritten when credentials for same user are stored. There are these phases in the test:

  1. Credentials file with initial password is created
  2. Read initial password
  3. Overwrite initial password with different one
  4. Read new password
  5. Verify that new password is different to initial one

Callbacks based test

var credentialsFile = 'testCredentials.txt';

var checkError = function(err, done) {
  if (err) {
    done(err);
  }
};

var readPassword = (credentialsFile, done, callback) => {
  fs.readFile(credentialsFile, (err, data) => {
    checkError(err, done);
    var jsonData = JSON.parse(data);
    callback(jsonData.user.password);
  });
};
describe('jasstor', () => {
  var jasstor = new Jasstor(credentialsFile);

  describe('when creadentials file already exist', () => {
    beforeEach(done => {
      fs.unlink(credentialsFile, () => {
        jasstor.saveCredentials('user', 'password', 'role', done);
      });
    });

    it('should overwrite existing password', done => {
      readPassword(credentialsFile, done, (originalPassword) => {
        jasstor.saveCredentials('user', 'password1', 'role', err => {
          checkError(err, done);
          readPassword(credentialsFile, done, (newPassword) => {
            newPassword.should.be.ok;
            originalPassword.should.be.ok;
            newPassword.should.not.equal(originalPassword);
            done();
          });
        });
      });
    });
  });
});

jasstor is testing object and jasstor.saveCredentials() is testing function. There is created helper function readPassword because password needs to be read twice during test. Pretty straight forward callbacks pyramid. I don’t like calling checkError at the beginning of each callback. Annoying Node pattern.

Promises based test

var fs = Bluebird.promisifyAll(require('fs'));
var credentialsFile = 'testCredentials.txt';

var readPassword = (credentialsFile, userName, done) => {
  return fs.readFileAsync(credentialsFile)
    .then(JSON.parse)
    .then(jsonData => {
      return jsonData[userName].password;
    }).catch(done);
};

var ignoreCallback = () => {};

describe('jasstor tested with promises', () => {
  var jasstor = Bluebird.promisifyAll(new Jasstor(credentialsFile));

  describe('when creadentials file already exist', () => {
    beforeEach(done => {
      fs.unlinkAsync(credentialsFile)
        .finally(() => {
          jasstor.saveCredentials('user', 'password', 'role', done);
        }).catch(ignoreCallback);
    });

    it('should overwrite existing password', done => {
      var originalPassword = readPassword(credentialsFile, 'user', done);
      var newPassword;
      jasstor.saveCredentialsAsync('user', 'password1', 'role')
        .then(() => {
          newPassword = readPassword(credentialsFile, 'user', done);
          newPassword.should.be.ok;
          originalPassword.should.be.ok;
          newPassword.should.not.equal(originalPassword);
          done();
        }).catch(done);
    });
  });
});

Important here is promisification of fs library (first line). It patches fs module to have additional methods with Async suffix. These methods return Promise and doesn’t take callback as parameter. This effectively converts existing API to promise based API. Same is done to testing object jasstor.

It was slight surprise to me that Promises actually doesn’t enable for less verbose code. Few facts are pretty obvious to me after this comparison:

  • Much more elegant error handling. As long as error callback has error as first parameter, you can just pass it to catch block as function reference.
  • Callbacks pyramid is flattened. This can improve readability. But readability is probably matter of maturity with certain approach.

Callback vs Promises – Node module comparison

Now I am going to compare code that was tested by tests above.

Callbacks based code

var hashPassword = (password, callback) => {
  bcrypt.genSalt(10, (err, salt) => bcrypt.hash(password, salt, callback));
};

var readJsonFile = (storageFile, callback) => {
  fs.exists(storageFile, (result) => {
    if (result === false) {
      callback(null, {});
    } else {
      fs.readFile(storageFile, (err, data) => {
        var jsonData = JSON.parse(data);
        callback(err, jsonData);
      });
    }
  });
};

module.exports = class Jasstor {
  constructor(storageFile) {
    this.storageFile = storageFile;
  }

  saveCredentials(user, password, role, callback) {
    readJsonFile(this.storageFile, (err, jsonData) => {
      hashPassword(password, (err, hash) => {
        jsonData[user] = {
          password: hash,
          role: role
        };
        var jsonDataString = JSON.stringify(jsonData);

        fs.writeFile(this.storageFile, jsonDataString, callback);
      });
    });
  }
};

Here we have ES6 class Jasstor with constructor and method that saves credentials into JSON file. There are two helper methods hashPassword and readJsonFile to help with repetitive tasks across the Jasstor class. We can see callback pyramid again. It is slightly simplified by helper functions.

Promises based code

var fs = Bluebird.promisifyAll(require('fs'));
var bcrypt = Bluebird.promisifyAll(require('bcrypt'));

var hashPassword = password => {
  return new Promise(resolve => {
    bcrypt.genSaltAsync(10)
      .then(salt => {
        return bcrypt.hashAsync(password, salt);
      }).then(resolve);
  });
};

var readJsonFile = storageFile => {
  return new Promise((resolve, reject) => {
    fs.exists(storageFile, result => {
      if (result === true) {
        fs.readFileAsync(storageFile)
          .then(JSON.parse)
          .then(resolve)
          .catch(reject);
      } else {
        resolve({});
      }
    });
  });
};

module.exports = class Jasstor {
  constructor(storageFile) {
    this.storageFile = storageFile;
  }

  saveCredentials(user, password, role, callback) {
    readJsonFile(this.storageFile).then(jsonData => {
      hashPassword(password).then(hash => {
        jsonData[user] = {
          password: hash,
          role: role
        };
        return jsonData;
      }).then(JSON.stringify)
        .then(jsonDataString => {
          fs.writeFile(this.storageFile, jsonDataString, callback);
        }).catch(callback);
    }).catch(callback);
  }
};

Same implementation packed into Promises is more verbose (hopefully I missed some tricks that could made it shorter). I like again simplified error handling. You maybe spot  that fs.exists isn’t promisified. If you take a look at its API, callback doesn’t have error as first parameter. I suspect, this is why fs.existsAsync doesn’t work correctly. Not sure if this is limitation of Bluebird promises library I am using or Promises A+ specification.

Conclusion

Promises are very nice approach that could totally change style of your programming. But I have to admit that I am not 100% sold to it yet. It took me some time to wrap my head around the concept. Promises also seem to me slightly more verbose than callbacks. When you have functions with one parameter and return value, you can nicely chain them with just passing function references into Promise chain. But mostly you don’t have such comfortable APIs and you end up doing “flattened callbacks pyramid”.

I would suggest to try Promises on your project (or small library) and make own opinion. Examples aren’t enough challenging to push the Promises into its limits.

3 thoughts on “Promises vs Callbacks – Code comparison

    1. I often hear similar statements from Java back-end developers that has limited experience with JavaScript. You would be surprised that JavaScript front end developers often has similar attitude to Java.

      My opinion is that language doesn’t matter. JavaScript actually is raising its significance from JQuery spaghetti code to serious front and also back end platform. Serious Java Web programmer should be skilled with JavaScript because common approach is now REST API on server + templating on client.
      Also Node.JS incorporates very handy paradigm of non-blocking I/O + single threaded event loop. This is more efficient under high load than classical multithreading. There are such systems in Java world (Akka, Spring Reactor) but don’t have wide spread usage.

      Also ask serious front end developers about tooling. There is big bang of front end tooling based on node and no other platform can compete with this power.

      And when somebody says to me that Node.JS is used only for small or learning projects, I would reply, you need to educate yourself more.
      Netflix, EBay, Paypal, WallMart are moving their server side rendering to Node. Are such sites big enough for your scale?

      I don’t believe that Node is going to rule the world. A lot of problems of Node platform are rock solid in Java environment and seem like baby problems. I bet it’s because of maturity differences of both worlds. Maintainability of Node server side code would be serious concern for me. But this can significantly change with adoption of ES6.

      I bet is that templating will be domain of isomorphic / client / server side JavaScript and Java back-end will stay dominant behind REST APIs.

      And Just to be clear, I AM MOSTLY SKILLED WITH BACK-END JAVA but am not that confident stay only in Java bubble.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.