Promises simple, straightforward and powerful way. It was build with less is more mantra in mind, API consist of just 7 functions which should give all you need to configure complicated asynchronous control flow.
This work is highly inspired by other deferred/promise implementations, in particular Q by Kris Kowal.
### Basics
When there's work to do that doesn't return immediately (asynchronous), deferred
object is created and promise (deferred.promise
) is returned to the world. When finally value is obtained, deferred is resolved with it deferred.resolve(value)
. At that point all promise observers (added in meantime via deferred.promise.then
) are notified with value of fulfilled promise.
Example:
var deferred = require('deferred');
var later = function () {
var d = deferred();
setTimeout(function () {
d.resolve(1);
}, 1000);
return d.promise;
};
later().then(function (n) {
console.log(n); // 1
});
promise
is really a then
function, so you may use it directly:
later()
(function (n) {
console.log(n); // 1
});
promise
takes callback and returns another promise. Returned promise will resolve with value that is a result of callback function, this way, promises can be chained:
later()
(function (n) {
var d = deferred();
setTimeout(function () {
d.resolve(n + 1);
}, 1000);
return d.promise;
})
(function (n) {
console.log(n); // 2
});
Callback passed to promise
may return anything, it may also be regular synchronous function:
later()
(function (n) {
return n + 1;
})
(function (n) {
console.log(n); // 2
});
Promises can be nested. If promise resolves with another promise, it's not really resolved. It's resolved only when final promise returns real value:
var count = 0;
var laterNested = function fn (value) {
var d = deferred();
setTimeout(function () {
value *= 2;
d.resolve((++count === 3) ? value : fn(value));
}, 1000);
return d.promise;
};
laterNested(1)(function (n) {
console.log(n); // 8
});
### Error handling
Promise is rejected when it's resolved with an error, same way if callback passed to promise
throws exception it becomes resolution of promise returned by promise
call. To handle error, pass second callback to promise
:
later()
(function (n) {
throw new Error('error!')
})
(function () {
// never called
}, function (e) {
// handle error;
});
When there is no error callback passed, error is silent. To expose error, end chain with .end()
, then error that broke the chain will be thrown:
later()
(function (n) {
throw new Error('error!')
})
(function (n) {
// never executed
})
.end(); // throws error!
end
takes optional handler so instead of throwing, error can be handled other way. Behavior is exactly same as when passing second callback to promise
:
later()
(function (n) {
throw new Error('error!')
})
.end(function (e) {
// handle error!
});
## Asynchronous functions as promises
There is a known convention in JavaScript for working with asynchronous calls. Following approach is widely used within node.js:
var afunc = function (x, y, callback) {
setTimeout(function () {
try {
callback(null, x + y);
} catch (e) {
callback(e);
}
}, 1000);
};
Asynchronous function receives callback argument, callback handles both error and success. There's easy way to turn such functions into promises and take advantage of promise design. There's deferred.asyncToPromise
for that, let's use shorter name:
var a2p = deferred.asyncToPromise;
// we can also import it individually:
a2p = require('deferred/lib/async-to-promise');
This method can be used in various ways.
First way is to assign it directly to asynchronous method:
afunc.a2p = a2p;
afunc.a2p(3, 4)
(function (n) {
console.log(n); // 7
});
Second way is more traditional (I personally favor this one as it doesn't touch asynchronous function):
a2p = a2p.call;
a2p(afunc, 3, 4)
(function (n) {
console.log(n); // 7
});
Third way is to bind method for later execution. We'll use ba2p
name for that:
var ba2p = require('deferred/lib/async-to-promise').bind;
var abinded = ba2p(afunc, 3, 4);
// somewhere in other context:
abinded()
(function (n) {
console.log(n); // 7
});
Note that this way of using it is not perfectly safe. We need to be sure that abinded
will be called without any not expected arguments, if it's the case, then it won't execute as expected:
abinded(7, 4); // TypeError: number is not a function.
Node.js example, reading file, changing it's content and writing under different name:
var fs = require('fs');
a2p(fs.readFile, __filename, 'utf-8')
(function (content) {
// change content
return content;
})
(ba2p(fs.writeFile, __filename + '.changed'))
.end();
## Control-flow, joining promises
There are three dedicated methods for joining promises. They're avaiable on deferred
as deferred.join
, deferred.all
and deferred.first
. Let's access them directly:
// let's access them directly:
var join = deferred.join;
var all = deferred.all;
var first = deferred.first;
As with other API methods, they can also be imported individually:
var join = require('deferred/lib/join/default')
, all = require('deferred/lib/join/all')
, first = require('deferred/lib/join/first');
Join methods take arguments of any type and internally distinguish between promises, functions and others. Call them with list of arguments or an array:
join(p1, p2, p3);
join([p1, p2, p3]); // same behavior
join
and all
return another promise, which resolves with combined result of resolved arguments:
join(p1, p2, p3)
(function (result) {
// result is array of resolved values of p1, p2 and p3.
});
first
results with value of first resolved argument:
first(p1, p2, p3)
(function (result) {
// result is resolved p1, p2 or p3, whichever was first
});
### Examples:
Regular control-flow
Previous read/write file example written with all
:
all(
a2p(fs.readFile, __filename, 'utf-8'),
function (content) {
// change content
return content;
},
ba2p(fs.writeFile, __filename + '.changed')
).end();
Concat all JavaScript files in given directory and save it to lib.js:
all(
// Read all filenames in given path
a2p(fs.readdir, __dirname),
// Filter *.js files
function (files) {
return files.filter(function (name) {
return (name.slice(-3) === '.js');
});
},
// Read files content
function (files) {
return join(files.map(function (name) {
return a2p(fs.readFile, name, 'utf-8');
}));
},
// Concat into one string
function (data) {
return data.join("\n");
},
// Write to lib.js
ba2p(fs.writeFile, __dirname + '/lib.js')
).end();
We can shorten it a bit with introduction of functional sugar, it's out of scope of this library but I guess worth an example:
var invoke = require('es5-ext/lib/Function/invoke');
all(
// Read all filenames in given path
a2p(fs.readdir, __dirname),
// Filter *.js files
invoke('filter', function (name) {
return (name.slice(-3) === '.js');
}),
// Read files content
invoke('map', function (name) {
return a2p(fs.readFile, name, 'utf-8');
}), join,
// Concat into one string
invoke('join', "\n"),
// Write to lib.js
ba2p(fs.writeFile, __dirname + '/lib.js')
).end();
#### Asynchronous loop
Let's say we're after content that is paginated over many pages on some website (like search results). We don't know how many pages it spans. We only know by reading page n whether page n + 1 exists.
First things first. Simple download function, it downloads page at given path from predefinied domain and returns promise:
var http = require('http');
var getPage = function (path) {
var d = deferred();
http.get({
host: 'www.example.com',
path: path
}, function(res) {
res.setEncoding('utf-8');
var content = "";
res.on('data', function (data) {
content += data;
});
res.on('end', function () {
d.resolve(content);
});
}).on('error', d.resolve);
return d.promise;
};
Deferred loop:
var n = 1, result;
getPage('/page/' + n++)
(function process (content) {
// populate result
// decide whether we need to download next page
if (isNextPage) {
return getPage('/page/' + n++)(process);
} else {
return result;
}
})
(function (result) {
// play with final result
}).end();
We can also make it with all
:
var n = 1, result;
all(
getPage('/page/' + n++),
function process (content) {
// populate result
// decide whether we need to download next page
if (isNextPage) {
return getPage('/page/' + n++)(process);
} else {
return result;
}
},
function (result) {
// play with final result
}
).end();
##### async.forEach, async.map, async.filter ..etc.
Asynchronous handlers for array iterators, forEach and map:
all(arr, function (item) {
// logic
return promise;
})
(function (results) {
// deal with results
// if it's forEach than results are obsolete
})
.end();
I decided not to implement array iterator functions in this library, for two reasons,
first is as you see above - it's very easy and straightforward to setup them with provided join methods, second it's unlikely we need most of them.