parley
Practical, lightweight flow control for Node.js
Usage
Parley helps you write functions that can be called like this:
doStuff({ foo: 123 })
.foo({ bar: 456 })
.exec(function (err, result){
});
Or like this:
doStuff({ foo: 123 })
.baz({ bar: 456 })
.then(function (result){
})
.catch(function(err) {
});
You can also obtain a promise simply by calling .toPromise()
.
Benchmarks
As of January 15, 2017:
baseline.benchmark.js
• • • • • •
• • o
• b e n c h m a r k s •
• (instantiation) °
------------------------------------
parley(handler)
• just_build#0 x 18,162,364 ops/sec ±0.98% (90 runs sampled)
✓ should be performant enough (using benchSync())
parley(handler).exec(cb)
• build_AND_exec#0 x 1,804,891 ops/sec ±1.77% (84 runs sampled)
✓ should be performant enough (using benchSync())
parley(handler, undefined, {...}) (w/ 9 custom methods)
• just_build_with_9_custom_methods#0 x 3,947,502 ops/sec ±1.62% (90 runs sampled)
✓ should be performant enough (using benchSync())
parley(handler, undefined, {...}).exec(cb) (w/ 9 custom methods)
• build_AND_exec_with_9_custom_methods#0 x 1,259,925 ops/sec ±2.08% (76 runs sampled)
✓ should be performant enough (using benchSync())
practical benchmark
• mock "find().exec()"#0 x 33.69 ops/sec ±0.98% (73 runs sampled)
✓ should be performant enough when calling fake "find" w/ .exec() (using bench())
• mock "find(..., explicitCb)"#0 x 33.93 ops/sec ±0.90% (73 runs sampled)
✓ should be performant enough when calling NAKED fake "find" (using bench())
• mock "validate().exec()"#0 x 789,446 ops/sec ±1.85% (92 runs sampled)
✓ should be performant enough when calling fake "validate" w/ .exec() (using benchSync())
• mock "validateButWith9CustomMethods().exec()"#0 x 686,544 ops/sec ±1.21% (90 runs sampled)
✓ should be performant enough calling fake "validateButWith9CustomMethods" w/ .exec() (using benchSync())
• mock "validate(..., explicitCb)"#0 x 10,157,027 ops/sec ±1.77% (87 runs sampled)
✓ should be performant enough when calling NAKED "validate" (using benchSync())
------------------------------------
• • • • • •
• • o
• < / b e n c h m a r k s > •
• °
o°
Help
If you have questions or are having trouble, click here.
Bugs 
To report a bug, click here.
Overview
This section offers a high-level look at how to use parley from both a userland and implementor perspective. You can also skip ahead to the API reference below.
Building a deferred object
Use parley to build a deferred object. This provides access to .exec()
, .then()
, .catch()
, and .toPromise()
, but you can also attach any extra methods you'd like to add.
var parley = require('parley');
var deferred = parley(function (done){
setTimeout(function (){
if (Math.random() > 0.5) {
return done(new Error('whoops, unlucky I guess'));
}
if (Math.random() > 0.2) {
return done(undefined, Math.floor(5*Math.random()));
}
return done();
}, 50);
});
For a more complete version of the above example, click here.
Results
To send back a result value from your handler, specify it as the second argument when invoking done
.
return done(undefined, 'hello world');
Depending on how userland code chooses to work with the deferred object, your result will be passed back to userland as either the second argument to the .exec()
callback, or as the value resolved from the promise.
.exec(function(err, result) {
});
.then(function(result) {
});
Errors
To send back an error from your handler, handle it in the conventional Node.js way.
return done(new Error('Oops'));
Depending on how userland code chooses to work with the deferred object, your error will be passed back to userland as either the first argument to the .exec()
callback, or as the promise's rejection "reason".
.exec(function(err, result) {
});
.catch(function(err) {
});
Negotiating errors
Sometimes, there is more than one exceptional exit a function might take. To make it possible for userland code to negotiate different exits from your function, give the error a code
property.
var x = Math.random();
if (x > 1) {
return done(new Error('Consistency violation: This should never happen.'));
}
var flaverr = require('flaverr');
if (x > 0.6) {
return done(flaverr('E_TOO_BIG', new Error('Oops: too big')));
}
if (x < 0.4) {
return done(flaverr('E_TOO_SMALL', new Error('Too small -- probably already in use!')))
}
Then in userland, this can be easily negotiated. Note that whether the code is using a Node-style callback or a promise, the approach is conceptually the same regardless.
.exec(function(err, result) {
if (err) {
switch(err.code) {
case 'E_TOO_BIG': return res.status(400).json({ reason: 'Ooh, too bad! '+err.message });
case 'E_TOO_SMALL': return res.status(401).json({ reason: 'Please try again later. '+err.message });
default:
console.error('Unexpected error:',err.stack);
return res.sendStatus(500);
}
}
});
.then(function (result) {
})
.catch({ code: 'E_TOO_BIG' }, function(err) {
return res.status(400).json({ reason: 'Ooh, too bad! '+err.message });
})
.catch({ code: 'E_TOO_SMALL' }, function(err) {
return res.status(401).json({ reason: 'Please try again later. '+err.message });
})
.catch(function(err) {
console.error('Unexpected error:',err.stack);
return res.sendStatus(500);
});
Handling uncaught exceptions
Out of the box, when using asynchronous callbacks in Node.js, if the code in your callback throws an uncaught error, the process will crash!
For example, the following code would crash the process:
setTimeout(function (){
JSON.parse('who0ps"thisis totally not valid js{}n');
return res.ok();
}, 50);
To protect against this, always be sure to use try...catch blocks around any logic
that might throw in an asynchronous, Node-style callback.
For example:
setTimeout(function (){
try {
JSON.parse('who0ps"thisis totally not valid js{}n');
} catch (e) { return res.serverError(e); }
return res.ok();
}, 50);
Here are a few common use cases to watch out for:
- basic JavaScript errors; e.g. syntax issues, or trying to use the dot (.) operator on
null
. - trying to JSON.parse() some data that is not a valid, parseable JSON string
- trying to JSON.stringify() a circular object
- RPS methods in Sails.js; e.g.
.publish()
, .subscribe()
, .unsubscribe()
- Waterline's
.validate()
model method - Node core's
assert()
- most synchronous methods from Node core (e.g.
fs.readFileSync()
) - any synchronous machine called with
.execSync()
- other synchronous functions from 3rd party libraries
Note that this is not an issue when using promises, since .then()
automatically catches uncaught errors
(although there are other considerations when using promises-- for instance, forgetting to use .catch()
each time .then() is used is a common source of hard-to-debug issues, technical debt, and memory leaks.)
EXPERIMENTAL: As of parley 2.3.x, there is a new, experimental feature that allows you to
easily provide an extra layer of protection: an optional 2nd argument to .exec()
. If specified,
this function will be used as an uncaught exception handler-- a simple fallback just in case something
happens to go wrong in your callback function.
This allows you to safely write code like the following without crashing the server:
User.create({ username: 'foo' }).exec(function (err, result) {
if (err) {
if (err.code === 'E_UNIQUE') { return res.badRequest('Username already in use.'); }
else { return res.serverError(err); }
}
var result = JSON.parse('who0ps"thisis totally not valid js{}n');
return res.ok(result);
}, res.serverError);
Of course, it's still best to be explicit about error handling whenever possible.
The extra layer of protection is just that-- it's here to help prevent issues
stemming from the myriad runtime edge cases it's almost impossible to anticipate
when building a production-ready web application.
Flow control
Since Node.js is asynchronous, seemingly-tricky flow control problems often arise in practical, userland code. Fortunately, they're easy to solve when equipped with the proper tools and strategies.
Most of the examples below use simple Node callbacks, but note that many similar affordances are available for promises -- for example, check out .toPromise()
(below) and Promise.all()
(in bluebird, or native in ES6, etc.). The concepts are more or less the same regardless.
Unless you and the rest of your team are experts with promises and already have tight, consistently-applied and agreed-upon conventions for how to implement the use cases below, you're probably best off using Node callbacks.
Async loops
Loop over many asynchronous things, one at a time, using async.eachSeries()
.
For this example, make sure you have access to the async
library:
var async = require('async');
var results = [];
async.eachSeries(['a','b','c','d','e','f','g','h','i','j','k','l'], function (letter, next) {
doStuff(letter).exec(function (err, resultForThisLetter){
if (err) { return next(err); }
results.push(resultForThisLetter)
return next();
});
},
function afterwards(err) {
if (err) {
console.error(err);
return res.sendStatus(500);
}
return res.json(results);
});
Async "if"
Even simple detours and conditionals can sometimes be tricky when things get asynchronous.
Fortunately, relatively concise and robust branching logic can be easily implemented using out-of-the-box JavaScript using this weird trick™.
User.findOne({ id: req.param('id') })
.exec(function(err, profileUser) {
if (err) { return res.serverError(err); }
if (!profileUser) { return res.notFound(); }
(function(proceed) {
if (!req.session.userId) {
return proceed();
}
User.findOne({ id: req.session.userId })
.exec(function (err, loggedInUser) {
if (err) { return proceed(err); }
if (!loggedInUser) { return proceed(new Error('Logged-in user ('+req.session.userId+') is missing from the db!')); }
return proceed(undefined, loggedInUser);
});
})(function afterwards(err, loggedInUser){
if (err) { return res.serverError(err); }
return res.view('profile', {
profile: _.omit(profileUser, ['password', 'email']),
me: loggedInUser ? _.omit(loggedInUser, 'password') : {}
});
});
});
More background on using the if/then/finally pattern for asynchronous flow control
Async recursion
Much like "if/then/finally" above, the secret to tidy asynchronous recursion is the (notorious) self-calling function.
#!/usr/bin/env node
var path = require('path');
var fs = require('fs');
(function _recursively(thisDir, done){
var pathToCheck = path.resolve(thisDir, './package.json');
fs.stat(pathToCheck, function(err) {
if (err) {
switch (err.code) {
case 'ENOENT':
var oneLvlUp = path.dirname(thisDir);
_recursively(oneLvlUp, function(err, nearestPJ) {
if (err) { return done(err); }
return done(undefined, nearestPJ);
});
return;
default: return done(err);
}
}
return done(undefined, pathToCheck);
});
})(process.cwd(), function afterwards(err, nearestPJ) {
if (err) {
console.error(err);
return process.exit(1);
}
console.log('Found nearest package.json file at:',nearestPJ);
});
More examples and thoughts on asynchronous recursion
Parallel processing / "races"
To manage "races" between deferred objects while still performing tasks simultaneously, you can use async.each()
-- for example, here's the async.eachSeries()
code from above again, but optimized to run on groups of letters simultaneously, while still processing letters within those groups in sequential order:
var results = [];
async.each(['abc','def','ghi','jkl'], function (group, next) {
var theseLetters = group.split('');
var resultsForThisGroup = [];
async.eachSeries(theseLetters, function (letter, next) {
doStuff(letter).exec(function (err, resultForThisLetter){
if (err) { return next(err); }
resultsForThisGroup.push(resultForThisLetter)
return next();
});
},
function (err) {
if (err) { return next(err); }
resultsForThisGroup.forEach(function(letter){
results.push(letter);
});
return next();
});
},
function afterwards(err) {
if (err) {
console.error(err);
return res.sendStatus(500);
}
return res.json(results);
});
More background on asynchronous vs. synchronous flow control in general
API reference
Implementor interface
parley()
Build and return a deferred object.
As its first argument, expects a function (often called the handler, or more specifically "handleExec") that will run whenever userland code executes the deferred object (e.g. with .exec()
).
var deferred = parley(function (done) {
return done();
});
This first argument is mandatory-- it defines what your implementation actually does when .exec()
is called.
Optional callback
There is also an optional second argument you can use: another function that, if provided, will cause your handler (the first arg) to run immediately.
This provides a simple, optimized shortcut for exposing an optional callback to your users.
Why bother? Well, for one thing, it's stylistically a good idea to give users a way to call your handler with as little sugar on top as possible. More rarely, for very performance-sensitive applications, direct callback usage does provide a mild performance benefit.
var deferred = parley(function (done){
}, optionalCbFromUserland);
Custom methods
The safest way to attach custom methods is by using parley's optional 3rd argument. The usual approach is for these custom methods to be chainable (i.e. return this
).
var privateMetadata = {};
var deferred = parley(function (done){
}, optionalCbFromUserland, {
someCustomMethod: function(a,b,c){
privateMetadata = privateMetadata || {};
privateMetadata.foo = privateMetadata.foo || 1;
privateMetadata.foo++;
return deferred;
}
});
Don't use this approach to define non-functions or overrides with special meaning (e.g. inspect
, toString
, or toJSON
).
To do that, just set the property directly-- for example:
deferred.inspect = function(){ return '[My cool deferred!]'; };
Userland interface
The deferred object returned by parley()
exposes a few different methods.
.exec()
parley(function(done){ return done(undefined, 1+1); })
.exec(function (err, result) {
});
parley(function(done){ return done(new Error('whoops'), 1+1); })
.exec(function (err, result) {
});
.then()
parley(function(done){ return done(undefined, 1+1); })
.then(function (result) {
});
.catch()
parley(function(done){ return done(new Error('whoops'), 1+1); })
.catch(function (err) {
});
.toPromise()
var promise1 = parley(function(done){ return done(undefined, 1+1); }).toPromise();
var promise2 = parley(function(done){ setTimeout(function(){ return done(); }, 10); }).toPromise();
Promise.all([
promise1,
promise2
])
.then(function(result){
}).catch(function (err) {
});
Other methods
Implementors may also choose to attach other methods to the deferred object (e.g. .where()
). See "Custom methods" above for more information.
Contributing

Please observe the guidelines and conventions laid out in the Sails project contribution guide when opening issues or submitting pull requests.

Pronunciation
/ˈpärlē/
Rather than picking barley and getting snarly, she decided to npm install parley
and listen to some Bob Marley.
License
This package, like the Sails framework, is free and open-source under the MIT License.