Writing a promise library - Part 2
In part 1 we looked at how to create a simple promise implementation that would allow converting a single node callback style function into a promise. The limitation of the implementation is that only one function can be called on resolution or rejection of the promise. Ideally we would like to chain multiple calls to then
to define a sequence of calls to make as each step gets resolved.
The main change that we have to make to the previous implementation is that then
needs to return a promise so that another call to then
can be chained on. We’re also going to create another class, defer
, which will contain a promise and handle the behavior when the promise is resolved or rejected.
To start, here is the updated promiseMaker
:
var promiseMaker = function() {
var promise = {
callbackFn: null,
errorFn: null,
status: 'pending',
type: 'promise'
};
promise.then = function(fn) {
var defer = deferMaker();
this.callbackFn = { func: fn, defer: defer };
return defer.promise;
};
promise.catch = function(fn) {
var defer = deferMaker();
this.errorFn = { func: fn, defer: defer };
return defer.promise;
};
promise.executeCallback = function(data, result) {
var res = data.func(result);
if (res && res.type === 'promise') {
data.defer.bind(res);
} else {
data.defer.resolve(res);
}
};
return promise;
};
There are a few changes compared to the previous promiseMaker
. First, each instance stores a status flag which will be one of "pending"
, "resolved"
or "rejected"
. The type
property just lets us determine whether an object is a promise or not. This lets us quickly check each return value in the promise chain to see if it is a promise and convert it into a promise if not. The executeCallback
method will invoke a callback then convert the output to a promise if necessary.
In this implementation the promise class will handle registering callbacks with then
and catch
and executing them. The defer class will contain the resolve
and reject
methods and is shown below.
var deferMaker = function() {
var defer = {
promise: promiseMaker()
};
defer.resolve = function(data) {
if (this.promise.callbackFn) {
this.promise.executeCallback(this.promise.callbackFn, data);
}
};
defer.reject = function(error) {
if (this.promise.errorFn) {
this.promise.executeCallback(this.promise.errorFn, error);
}
};
defer.bind = function(promise) {
var self = this;
if (promise.status === 'rejected') {
promise.catch(function(err) { self.reject(err); });
} else {
promise.then(function(data) { self.resolve(data); });
}
};
return defer;
};
The resolve
and reject
methods are not too different than in the previous implementation in that they invoke either the callback or the error handler. The bind
method lets a promise propagate it’s result to the next defer.
And some example usage:
First promisify some node functions.
var fs = require('fs');
var readFile = function(filename) {
var defer = deferMaker();
fs.readFile(filename, function(err, contents) {
if (err) {
defer.reject(err);
} else {
defer.resolve(contents.toString());
}
});
return defer.promise;
};
var writeFile = function(filename, data) {
var defer = deferMaker();
fs.writeFile(filename, data, function(err) {
if (err) {
defer.reject(err);
} else {
defer.resolve(filename);
}
});
return defer.promise;
};
$ echo "hello world" > test.txt
Then chain together a few promises.
readFile('test.txt')
.then(function(data) {
return writeFile('output.txt', data);
})
.then(function(data) {
console.log('wrote to', data);
});
## wrote to output.txt
The next part of this series will deal with propagating errors so that one call to catch
can be used to handle errors anywhere in the promise chain. See that in part 3.