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
- Two implementations of same module
- Two unit tests for the same module
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:
- Traceur – for ES6 transpilation
- Bluebird – promises library
- Mocha and Should for unit testing
- node.bcrypt.js – for hashing passwords
I decided to have these constraints for Promises
- Mocha will be excluded from promisification, so
describe
andit
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:
- Credentials file with initial password is created
- Read initial password
- Overwrite initial password with different one
- Read new password
- 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.