Package Exports
- deferred
- deferred/lib/async-to-promise
- deferred/lib/deferred
- deferred/lib/is-promise
- deferred/lib/promise
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (deferred) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
Asynchronous JavaScript with deferred and promises
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.
- Installation
- Deferred/Promise concept
- Asynchronous functions as promises
- Control-flow, sophisticated chaining
- Comparision to other solutions that take non promise approach
It's plain EcmaScript, but out of the box currently works only with node & npm (due to it's CommonJS package):
$ npm install deferred
For browser or other environments it needs to be bundled with few dependencies from es5-ext project (code states specifically which). Browser ready files will be available in near future.
## Deferred/Promise concept ### BasicsStraight to the point: 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 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
});
then
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()
.then(function (n) {
var d = deferred();
setTimeout(function () {
d.resolve(n + 1);
}, 1000);
return d.promise;
})
.then(function (n) {
console.log(n); // 2
});
Callback passed to then
may return anything, it may also be regular synchronous function:
later()
.then(function (n) {
return n + 1;
})
.then(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).then(function (n) {
console.log(n); // 8
});
Promise can be resolved only once, and callbacks passed to then
are also called only once, no exceptions. For deeper insight into this concept, and to better understand design decisions please see Kris Kowal design notes, it's well worth read.
Promise is rejected when it's resolved with an error, same way if callback passed to then
throws exception it becomes resolution of promise returned by then
. To handle error, pass second callback to then
:
later()
.then(function (n) {
throw new Error('error!')
})
.then(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()
.then(function (n) {
throw new Error('error!')
})
.then(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 then
:
later()
.then(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).then(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).then(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().then(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, see:
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')
.then(function (content) {
// change content
return content;
})
.then(ba2p(fs.writeFile, __filename + '.changed'))
.end();
## Control-flow, sophisticated chaining
There are three dedicated methods for constructing flow chain.
They're avaiable on deferred
:
deferred.join(...);
deferred.all(...);
deferred.first(...);
// let's access them directly:
var join = deferred.join;
var all = deferred.all;
var first = deferred.first;
join(p1, p2, p3, ...).then(...);
all(p1, p2, p3, ...).then(...);
first(p1, p2, p3, ...).then(...);
As with other API methods, they can also be imported individually:
var join = require('deferred/lib/chain/join')
, all = require('deferred/lib/chain/all')
, first = require('deferred/lib/chain/first');
Chain 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(...)
join
returns promise which resolves with an array of resolved values of all arguments.
Values may be anything, also errors (rejected promises, functions that thrown errors, errors itself). Returned promise always fulfills, never rejects.
Same as join
, with that difference that all arguments need to be succesful.
If there's any error, chain execution is stopped (following functions are not called), and promise is rejected with error that broke the chain. In succesful case returned promise value is same as with join
, array of results.
Fulfills with first succesfully resolved argument. If all arguments fail, then promise rejects with error that occurred last.
### Non promise argumentsAs mentioned above, chain methods take any arguments, not only promises. Function arguments are called with previous argument, if one resolved succesfully. If previous argument failed then function is never called. Error that rejected previous argument becomes also result of following function within returned result array. Any other values (neither promises or functions) are treated as if they were values of resolved promises.
### 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();
invoke
implementation can be found in es5-ext
project: https://github.com/medikoo/es5-ext/blob/master/lib/Function/invoke.js
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++)
.then(function process (content) {
// populate result
// decide whether we need to download next page
if (isNextPage) {
return getPage('/page/' + n++).then(process);
} else {
return result;
}
})
.then(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++).then(process);
} else {
return result;
}
},
function (result) {
// play with final result
}
).end();
### Comparision to other solutions that take non promise approach
Following are examples from documentation of other solutions rewritten deferred/promise way. You'll be the judge, which solution you find more powerful and friendly.
#### Step -> https://github.com/creationix/stepFirst example from Step README, using chained promises:
all(
a2p(fs.readFile, __filename, 'utf-8'),
function capitalize (txt) {
return txt.toUpperCase();
},
function showIt (newTxt) {
console.log(newTxt);
}
).end();
Again we can make it even more concise with functional sugar:
all(
a2p(fs.readFile, __filename, 'utf-8'),
invoke('toUpperCase'),
console.log
).end();
#### Async -> https://github.com/caolan/async
##### async.series:
all(
function () {
// do some stuff ...
return 'one';
},
function () {
// do some more stuff
return 'two';
}
)
.then(function (results) {
// results is now equal to ['one', 'two']
},
function (err) {
// handle err
});
##### async.paralles:
For parallel execution we pass already initialized promises:
all(
promise1,
a2p(asyncFunc, arg1, arg2),
promise2
)
.then(function (results) {
// results are resolved values of promise1, asyncFunc and promise2
},
function (err) {
// handle err
});
##### async.waterfall:
Resolved values are always passed to following functions, so again we have it out of a box:
all(
function () {
return ['one', 'two'];
},
function (args) {
return 'three';
},
function (arg1) {
// arg1 now equals 'three'
}
);
##### async.auto
It's a question of combining all
chains. First example from docs:
all(
all(
a2p(get_data),
a2p(make_folder)
),
ba2p(write_file)
ba2p(email_link)
).end();
##### async.whilst, async.until
See Asynchronous loop example, it shows how easily loops can be configured.
##### async.forEach, async.map, async.filter ..etc.Asynchronous handlers for array iterators, forEach and map:
all(arr.map(function (item) {
// logic
return promise;
})
.then(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 chain methods, second it's unlikely we need most of them.