Use flow.sync(fn) to turn asynchronous function into a new one, that can be used as if it's synchronous. Result of callback will be returned as return value, in case of error it will be thrown, so You can use try/catch to catch it.

You can also use flow.sync(obj, fname1, fname2, ...) to synchronize methods of object.

Synchronized functions are backward compatible and can be used as usual asynchronous functions, it's also can be mixed with other synchronous or asynchronous code. So You can introduce it to Your project gradually, by small steps.

The project hosted on GitHub You can report bugs and discuss features on the Issues page and install it with npm install control-flow command.


Usage patterns


Use yield keyword to pause execution (without blocking Node.js) and wait for asynchronous result.

flow.sync(fs, 'readFile')

var data = yield(fs.readFile(fname))
fs.readFile(fname, function(err, data)){}

var data = yield(fs.readFile(fname, flow.promise()))

Synchronize functions.

After it You can use it in both ways - synchronously or asynchronously (it stays backward compatible with standard asynchronous calls).

Or, You can provide promise by hand.

Basic usage, printing content of file


var flow = require('control-flow')
var fs   = require('fs')

flow.sync(fs, 'readFile')

flow.fiber(function(){
  var data = yield(fs.readFile(__filename, 'utf8'))
  console.log(data)

  try {
    data = yield(fs.readFile('invalid', 'utf8'))
  } catch (err) {
    console.log(err)
  }

  fs.readFile(__filename, 'utf8', function(err, data){
    console.log(data)
  })
})

In order to use synchronized functions and pause execution without blocking Node.js we need to wrap execution into Fiber.

Inside of Fiber we can use yield and call asynchronous functions as if it's synchronous.

We can also use standard try/catch statement to catch asynchronous errors.

Or call readFile asynchronously if we wish so.

Listing and printing files in directory


Listing content of current directory, checking if path is file and printing its content to console.

Using synchronize

var flow = require('control-flow')
var fs   = require('fs')

flow.sync(fs, 'readdir', 'stat', 'readFile')

flow.fiber(function(){
  var i, paths, path, stat, data
  paths = yield(fs.readdir('.'))
  for(i = 0; i < paths.length; i++){
    path = paths[i]
    stat = yield(fs.stat(path))
    if(!stat.isFile()) continue
    data = yield(fs.readFile(path, 'utf8'))
    console.log(data)
  }
})

The same code without synchronization

var fs = require('fs')

var printFile = function(paths, i){
  if(i >= paths.length) return
  var path = paths[i]
  fs.stat(path, function(err, stat){
    if(err) throw err
    if(stat.isFile()){
      fs.readFile(path, 'utf8', function(err, data){
        if(err) throw err
        console.log(data)
        printFile(paths, i + 1)
      })
    } else {
      printFile(paths, i + 1)
    }
  })
}

fs.readdir('.', function(err, paths){
  if(err) throw err
  printFile(paths, 0)
})

Usage with Express.js


var flow    = require('control-flow')
var fs      = require('fs')
var express = require('express')

flow.sync(fs, 'readFile')

var app = express.createServer()
app.use(function(req, res, next){
  flow.fiber(next)
})

app.get('/', function(req, res){
  var data = yield(fs.readFile(__filename, 'utf8'))
  res.send(data, {'Content-Type': 'text/plain'})
})

app.listen(3000)

Synchronized code can be mixed with asynchronous code in any combination.

Here are one of possible way to use it with Express.js.

Usage with Mocha.js


var flow = require('control-flow')
var fs   = require('fs')

flow.sync(fs, 'readFile')

flow.it = function(desc, callback){
  it(desc, function(done){
    flow.fiber(callback.bind(this), done)
  })
}

describe('File System', function(){
  flow.it('should read file', function(){
    var data = yield(fs.readFile(__filename, 'utf8'))
  })
})

You can define flow.it helper and use it to define synchronous specs.

You may also take a look at real-life test scenario that uses synchronize to simplify asynchronous calls for MongoDB.

ES6 Generators compatibility


Fibers are superset of ES6 Generators, so it's possible to design API in the same way as as if Generators would be already available.

I try to keep API as close to ES6 Generators specification as possible. When generators will be finally available in V8 & Node.js (in about 1-2 years) the API will be almost the same.

I believe the only thing that would needs to be changed is - to remove brackets and change yield(fn) to yield fn.


Copyright Alexey Petrushin, released under MIT License

Fork me on GitHub