Am creating this issue so that I can link to it from documentation etc.
Right now, the directory transformer function signature looks like this:
function myPlugin ( inputdir, outputdir, options, callback, errback ) {
// code goes here...
}
This is rather un-node-like. Most asynchronous node functions take a single callback that serves a dual purpose - if it's called with an argument, it's an errback, otherwise it's regular continuation passing style (CPS). The merits or otherwise of that convention are out of scope for this issue - that's how it is in node, and that's that. The question is whether Gobble should do it that way.
Motivation for the callback, errback style
Let's say we used the node convention. In simple cases where you're dealing with other functions that adhere to the CPS convention, that'd be nice and simple, because you could do
function myPlugin ( inputdir, outputdir, options, callback ) {
// code goes here...
fs.writeFile( path.join( outputdir, options.dest ), callback );
}
But in real life you'll want to handle the result yourself if:
- you want to intercept the error and handle it (e.g. passing a more informative error)
- you are writing multiple files, because no one
writeFile
operation can own the callback
- you are using promises
The last of these - promises - provided the original motivation for the callback, errback style. If you had a single (dual-purpose) callback, but you were using a promise-based fs
wrapper (I wrote such a thing - sander - precisely because fs
is rather low-level, and becomes a bit painful once you try and do anything meaningful with it), you'd have to write code like this:
function myPlugin ( inputdir, outputdir, options, callback ) {
// code goes here...
sander.writeFile( outputdir, options.dest ).then( callback, callback );
}
That's weird. And if you couldn't remember whether that function resolved with a value or not, and couldn't be bothered to look it up, you'd probably do this instead to ensure that an 'error' (the value the promise resolved with) wasn't wrongly passed to the callback:
function myPlugin ( inputdir, outputdir, options, callback ) {
// code goes here...
sander.writeFile( outputdir, options.dest ).then( function () {
callback();
}, callback );
}
If callback
and errback
are separate, dedicated functions, that don't violate the single responsibility principle, then you'd write this instead:
function myPlugin ( inputdir, outputdir, options, callback, errback ) {
// code goes here...
sander.writeFile( outputdir, options.dest ).then( callback, errback );
}
In essence, callback
and errback
are equivalent to a promise's fulfil
and reject
.
I like promises...
...and as of ES6 they're the 'correct' way to handle asynchronicity in JavaScript. They're much easier to combine and manipulate, and they result in much nicer code in almost all cases. But as the code above shows, CPS and promises are at odds with each other.
Options
- Use the conventions of the language, i.e. promises, and make life a bit more difficult for people using regular Node functions.
- Use the conventions of node, and use CPS - making life more difficult for people who use promises.
- A hybrid approach - use a single callback, but allow plugin authors to return a promise instead (i.e. if the transformer returns a 'thenable', attach
callback
and errback
to it, and disregard them if they're called within the transformer).
Any thoughts?