Promises in a simple and powerful way. It was built with the less is more mantra in mind. It's just few functions that should give all you need to easily configure complicated asynchronous control flow.
## Example
Concat all JavaScript files in a given directory and save it to lib.js.
Plain Node.js:
var fs = require('fs')
, readdir = fs.readdir
, readFile = fs.readFile
, writeFile = fs.writeFile
readdir(__dirname, function (err, files) {
var result, waiting;
if (err) {
throw err;
}
files = files.filter(function (file) {
return (file.slice(-3) === '.js') && (file !== 'lib.js');
});
waiting = 0;
result = [];
files.forEach(function (file, index) {
++waiting;
readFile(file, 'utf8', function (err, content) {
if (err) {
throw err;
}
result[index] = content;
if (!--waiting) {
writeFile(__dirname + '/lib.js', result.join("\n"), function (err) {
if (err) {
throw err;
}
});
}
});
});
});
Promises approach:
var deferred = require('deferred')
, fs = require('fs')
, readdir = deferred.promisify(fs.readdir)
, readFile = deferred.promisify(fs.readFile)
, writeFile = deferred.promisify(fs.writeFile);
writeFile(__dirname + '/lib.js',
readdir(__dirname)
.invoke('filter', function (file) {
return (file.slice(-3) === '.js') && (file !== 'lib.js');
})
.map(function (file) {
return readFile(file, 'utf-8');
})
.invoke('join', '\n')
).end();
### Deferred
For work that doesn't return immediately (asynchronous) you may create deferred object. Deferred holds both resolve
and promise
objects. Observers interested in value are attached to promise
object, with resolve
we resolve promise with an actual value. In common usage promise
is returned to the world and resolve
is kept internally
Let's create delay
function decorator. Decorates function so its execution is delayed in time:
var deferred = require('deferred');
var delay = function (fn, timeout) {
return function () {
var d = deferred(), self = this, args = arguments;
setTimeout(function () {
d.resolve(fn.apply(self, args));
}, timeout);
return d.promise;
};
};
var delayedAdd = delay(function (a, b) {
return a + b;
}, 100);
var resultPromise = delayedAdd(2, 3);
console.log(deferred.isPromise(resultPromise));
resultPromise(function (value) {
console.log(value);
});
### Promise
Promise is an object that represents eventual value which may already be available or is expected to be available in a future. Promise may succeed (fulfillment) or fail (rejection). Promise can be resolved only once.
In deferred
(and most of the other promise implementations) you may listen for the value by passing observers to then
function:
promise.then(onsuccess, onfail);
In deferred promise is really a then
function, so you may use promise function directly:
promise === promise.then;
promise(onsuccess, onfail);
However if you want to keep clear visible distinction between promises and other object I encourage you to always use promise.then
notation.
Both callbacks onsuccess
and onfail
are optional. They will be called only once aand only either onsuccess
or onfail
will be called.
#### Chaining
Promises by nature can be chained. promise
function returns another promise which is resolved with a value returned by a callback function:
delayedAdd(2, 3)(function (result) {
return result * result
})(function (result) {
console.log(result);
});
It's not just function arguments that promise function can take, it can be other promises or any other JavaScript value (however null
or undefined
will be treated as no value). With such approach you may override result of a promise chain with specific value. It may seem awkward approach at first, but it can be handy when you work with sophisticated promises chains.
#### Error handling
Errors in promises are handled with separate control flow, that's one of the reasons why code written with promises is more readable and maintanable than when using callbacks approach.
A promise resolved with an error (rejected), propagates its error to all promises that depend on this promise (e.g. promises initiated by adding observers).
If observer function crashes with error or returns error, its promise is rejected with the error.
To handle error, pass dedicated callback as second argument to promise function:
delayedAdd(2, 3)(function (result) {
throw new Error('Error!')
})(function () {
}, function (e) {
});
#### Ending chain
When there is no error callback passed, eventual error is silent. To expose the error, end promise chain with .end()
, then error that broke the chain will be thrown:
delayedAdd(2, 3)
(function (result) {
throw new Error('Error!')
})(function (result) {
})
.end();
It's very important to end your promise chains with end
otherwise eventual errors that were not handled will not be exposed.
end
is an exit from promises flow. You can call it with one callback argument and it will be called same way as callback passed to Node.js style asynchronous function:
promise(function (value) {
}).end(function (err, result) {
if (err) {
return;
}
});
Altenatively you can pass two callbacks onsuccess and onerror and that will resemble way .then
works, with difference that it won't extend chain with another promise:
promise(function (value) {
}).end(function (result) {
}, function (err) {
});
Just onerror may be provided:
promise(function (value) {
}).end(null, function (err) {
});
and just onsuccess either (we need to pass null
as second argument)
promise(function (value) {
}).end(function (res) {
}, null);
## Promisify - working with asynchronous functions as we know it from Node.js
There is a known convention (coined by Node.js) for working with asynchronous calls. The following approach is widely used:
var fs = require('fs');
fs.readFile(__filename, 'utf-8', function (err, content) {
if (err) {
return;
}
});
An asynchronous function receives a callback argument which handles both error and expected value.
It's not convienient to work with both promises and callback style functions. When you decide to build your flow with promises don't mix both concepts, just promisify
asynchronous functions so they return promises instead.
var deferred = require('deferred')
, fs = require('fs')
, readFile = deferred.promisify(fs.readFile);
readFile(__filename, 'utf-8')(function (content) {
}, function (err) {
});
With second argument passed to promisify
we may specify length of arguments that function takes before callback argument. It's very handy if we want to work with functions that may call our function with unexpected arguments (e.g. Array's forEach
or map
)
promisify
also takes care of input arguments. It makes sure that all arguments that are to be passed to asynchronous function are first resolved.
## Grouping promises
Sometimes we're interested in results of more than one promise object. We may do it again with help deferred
function:
deferred(delayedAdd(2, 3), delayedAdd(3, 5), delayedAdd(1, 7))(function (result) {
console.log(result);
});
### Map
It's analogous to Array's map, with that difference that it returns promise (of an array) that would be resolved when promises for all items are resolved. Any error that would occur will reject the promise and resolve it with same error.
Let's say we have list of filenames and we want to get each file's content:
var readFile = deferred.promisify(fs.readFile);
deferred.map(filenames, function (filename) {
return readFile(filename, 'utf-8');
})(function (result) {
});
map
is also available directly on a promise object, so we may invoke it directly on promise of a collection.
Let's try again previous example but this time instead of relying on already existing filenames, we take list of files from current directory:
var readdir = deferred.promisify(fs.readdir)
, readFile = deferred.promisify(fs.readFile);
readdir(__dirname).map(function (filename) {
return readFile(filename, 'utf-8');
})(function (result) {
});
There are cases when we don't want to run too many tasks simultaneously. Like common case in Node.js when we don't want to open too many file descriptors. deferred.map
accepts fourth argument which is maximum number of tasks that should be run at once:
deferred.map(filenames, function (filename) {
return readFile(filename, 'utf-8');
}, null, 100)(function (result) {
});
### Reduce
It's same as Array's reduce with that difference that it calls callback only after previous accummulated value is resolved, this way we may accumulate results of collection of promises or invoke some asynchronous tasks one after another.
deferred.reduce([delayedAdd(2, 3), delayedAdd(3, 5), delayedAdd(1, 7)], function (a, b) {
return delayedAdd(a, b);
})
(function (result) {
console.log(result);
});
As with map
, reduce
is also available directly as an extension on promise object.
### invoke
Schedule function call on promised object
var promise = deferred({ foo: function (arg) { return arg*arg; } });
promise.invoke('foo', 3)
(function (result) {
console.log(result);
});
var promise = deferred({ foo: function (arg, callback) {
setTimeout(function () {
callback(null, arg*arg);
}, 100);
} });
promise.invoke('foo', 3)
(function (result) {
console.log(result);
});