:bulb: Powerful control flow using ES6 generators
Powerful control flow using generators
watt
lets you write your async Javascript as if it were synchronous, which results in much simpler, more readable code.
ES6 introduced generators, which are functions that can be paused and resumed using the yield
keyword. This lets us do some cool things, for instance letting a library handle control flow.
Features:
for
/while
loops and if
statements, rather than needing the async
packagereturn
try
/catch
to handle async errorsthrow
watt
functions can be called with callbacks, you don’t have to change your APIBefore watt
:
function copyFile (source, dest, callback) {
fs.exists(source, function (exists) {
if (!exists) return callback('Source does not exist')
fs.exists(dest, function (exists) {
if (exists) return callback('Dest already exists')
fs.readFile(source, function (err, data) {
if (err) return callback(err)
fs.writeFile(dest, data, function (err) {
return callback(err)
})
})
})
})
}
After watt
:
var copyFile = watt(function * (source, dest, next) {
if (!(yield fs.exists(source, next.arg(0)))) throw 'Source does not exist'
if (yield fs.exists(dest, next.arg(0))) throw 'Dest already exists'
var data = yield fs.readFile(source, next)
yield fs.writeFile(dest, data, next)
})
Both of these can be called with:
copyFile('a', 'b', function (err) {
if (err) return console.error(err)
console.log('copied file')
})
npm install watt
watt( generatorFn([args...],next), [opts] )
Wraps a generator function and returns a callable function. The returned function can be called with fn([args...], [callback])
, and ([args...], next)
will be passed to generatorFn
.
The user-supplied callback
is removed from args
and will not be passed to the generator. callback
will be automatically called with callback(error, returned)
after the generator returns or throws an error. If no callback is supplied (the last argument is not a function), a Promise will be returned instead. Note: if you don’t want the last argument to be treated as a callback even if it is a function, you may set the noCallback
option (see below).
In the generator, yield
should be called to wait for an async thing to happen, and next
should be called to resume the generator. Alternatively, if a Promise is passed to yield
, the generator will automatically resume once the Promise resolves (or will throw an error if it rejects).
opts
may be an object with the following properties:
{
noCallback: Boolean, // default: false
// if true, the user-supplied `callback` will not be removed from the arguments,
// and will not be called after the generator returns or throws an errors
prepend: Boolean, // default: false
// if true, the generator is called with `generator(next, args...)`
// instead of `generator(args..., next)`. This can be useful for functions
// that accept varying numbers of arguments
context: Object, // default: caller scope
// sets the scope for the generator to run in. Similar to binding a function
// with fn.bind(context)
}
next(error, result)
The next
function is passed to watt
generators, and is used to unpause the generator after it has yield
ed. next
should be passed to async functions to “return” their results to the generator and resume execution.
If error
is truthy, the generator will throw an error. Otherwise, result
will be passed to the most recent yield
expression.
next.error(error)
If error
is truthy, the generator will throw an error. This is useful when you want your generator to throw an error after an error
event. For example:
var stream = fs.createReadStream('file.txt')
stream.on('error', next.error)
stream.pipe(someWritableStream)
yield someWritableStream.on('end', next)
In this example, if stream
encounters an error while we are waiting for it to pipe to someWritableStream
, we will abort waiting for the piping to finish and will throw the error.
next.arg(n, [ignoreError])
A function that returns a callback which can be supplied to async functions to get the n
th argument. Used as an alternative to next
, which defaults to the 1st argument (the 0th argument is the error).
If ignoreError
is truthy, the 0th argument won’t be thrown as an error. If n
is 0, ignoreError
is implied to be true
.
For example if we want to call request(url, cb)
which calls cb
with cb(err, res, body)
, and we want the body, we can do:
var body = yield request(url, next.arg(2))
next.args()
A callback which can be supplied to async functions to get all of the arguments passed to it. This function does not do any automatic error handling, since the error will be included in the arguments returned. The result returned is the function’s arguments
object.
Example:
var args = yield fs.readFile('file.txt', next.args)
var error = args[0]
var data = args[1]
next.parallel()
, next.sync()
Call next.parallel()
to execute async calls in parallel, then call yield next.sync()
to wait for these tasks to finish. The result of next.sync()
will be an array of the result values of tasks, in the order they were spawned. next.sync()
will unyield as soon as a task gives an error, or when all the tasks have finished.
next.parallel()
returns a callback similar to next
, which treats the first value as an error and the second as the return value. Like next
, this callback also has the arg
and args
properties.
Note that you should not yield
before a parallel call since we don’t want the generator to block until the call to sync
.
Example:
for (var i = 0; i < 5; i++) {
// waits a random amount of time, then returns i
setTimeout(next.parallel().arg(0), Math.random() * 1000, i)
}
// wait until all timeouts have finished
var res = yield next.sync()
// res is: [ 0, 1, 2, 3, 4 ]
watt.wrapAll(object, [opts], [names...])
Wraps generator function properties of object
. Each wrapped generator function gets bound to the context of object
. If no values are specified for names
, all generator function properties are wrapped. If one or more strings are specified for names
, only the properties with those keys will be wrapped.
opts
can be an options object that will be passed to watt()
.
This can be useful for wrapping generator methods of a class (call watt.wrapAll(this)
in the constructor).
var watt = require('watt')
class MyClass {
constructor () {
this.a = 5
// do this to wrap both 'foo' and 'bar'
watt.wrapAll(this)
// do this to wrap only 'foo'
watt.wrapAll(this, 'foo')
}
// remember to prefix with * to make a generator
* foo (next) {
yield doAsyncThing(this.a, next)
return yield doAnotherAsyncThing(next)
}
* bar (next) {
yield foo()
return yield doAnotherAsyncThing(next)
}
var mc = new MyClass()
mc.foo((err, res) => { ... })
}
Iterate through an array of file paths until we find one that exists.
Before watt
:
Without watt
, we need to use the async
module.
var async = require('async')
function firstExisting (paths, cb) {
async.eachSeries(paths, function (path, next) {
fs.exists(path, function (err, exists) {
if (err) return cb(err)
if (exists) return cb(path)
next()
})
})
}
After watt
:
With watt
, we can use a standard Javascript for
loop.
var async = require('watt')
var firstExisting = async(function * (paths, next) {
for (var path of paths) {
if (yield fs.exists(path, next)) return path
}
})
Print numbers from 0 to n, one per second.
Before watt
:
Without watt
, we need to use recursion. This isn’t too complex, however it is slightly less readable than it would be if it were synchronous.
var async = require('async')
function countUp (n, cb) {
function next (i) {
setTimeout(function () {
console.log(i)
if (i < n) next(i + 1)
}, 1000)
}
next(0)
}
After watt
:
With watt
, we can use a standard Javascript for
loop.
var async = require('watt')
var countUp = async(function * (n, next) {
for (var i = 0; i <= n; i++) {
yield setTimeout(next, 1000)
console.log(i)
}
})
if
statementsAn ugly part of using async callbacks is that the syntax gets ugly when branching. Consider a function that calls an async function foo
, then depending on the result maybe calls bar
, then finally calls baz
:
Before watt
:
function myAsyncFunc (cb) {
foo(function (err, res) {
if (err) return cb(err)
if (res) {
bar(function (err) {
if (err) return cb(err)
baz(cb)
})
return
}
baz(cb)
})
}
After watt
:
var myAsyncFunc = async(function * (next) {
var res = yield foo(next)
if (res) yield bar(next)
return yield baz(next)
}
watt
works well with Promises. If you yield
a promise, its result will be returned if is resolves, or its error will be thrown if it is rejected.
Additionally, watt
functions return Promises if a callback is not provided, so you can call them from another watt
generator without providing the next
argument.
var foo = async(function * (next) {
setTimeout(next, 1000)
return 'aslfdkj'
}
var bar = async(function * (next) {
return yield foo() // 'next' not necessary
})
// callers can use the Promise API instead of a callback if they want
bar().then(res => console.log(res))
co
co
is similar to watt
, but the main difference is that it only works with Promises. It requires that you convert callback functions to return Promises before you can call them, and it does not let you wrap generators with a callback API.