In node.js asynchronous operations notify about completion by calling callback function. Usually the first argument of this function is error and the rest (most of the callbacks has two arguments) is a function-specific data. The signature of the callback function is (Error, any) => void.

// callback for the file reading operation
const print = (err, data) => console.log(data);

require('fs').readFile('text.txt', {encoding:'utf8'}, print);

Let's create the function value that converts the given value to function with one callback argument:

const value = v => cb => cb(null, v);

Its type is any => (((error, any) => void) => void). The function receives a value and returns function that receives callback and immediately runs it, providing the given value as its result.

This function makes calls to asynchronous functions replaceable with values without affecting the control flow. In some sense it is a mock function for testing callback.

value('ok')(print);

Now construct the read and write functions that receive and return values:

var read = filepath =>
  callback =>
    filepath((_, filepath) => 
      fs.readFile(
        filepath,
        { encoding : 'utf8' },
        callback));

var write = (filepath, data) =>
  callback =>
    filepath((_, filepath) =>
      data((_, data) =>
        fs.writeFile(filepath, data, callback)));

The error handling is skipped to keep the code simple.

It is now possible to read one file and save its content to another by calling asynchronous read and write similarly to their synchronous counterparts:

write(value('file.out'), read(value('file.in'))();

The trailing () is not a mistake - it invokes write.

Constructing a library of async functions wrapped following this pattern makes possible to write code using these functions like if there are synchronous:

const tokens = readTokensFromDb(value('user'));
const html = replaceTokens(read(value('template.html')), tokens);
sendEmail(html)();

where readTokensFromDb, read, replaceTokens, sendEmail are asynchronous.

The first version of the functional asynchronous read and write require simple values to be wrapped into the callback function.

var value = value => result => result(null, value);
save(value('test1.out'), value('ok'))();

Of course, the implementation of the read and write can help developer a bit and accept both simple and callback values:

var read = filepath =>  
  callback => {
    let impl = filepath => {
      let options = { encoding : 'utf8' };
      fs.readFile(filepath, options, callback);
    };
    return ('function' === typeof filepath)
      ? filepath((_, filepath) => impl(filepath))
      : impl(filepath);
  };

var write = (filepath, data) =>
  callback => {
    let impl1 = (filepath, data) =>
      fs.writeFile(filepath, data, callback);
    let impl2 = filepath =>
      ('function' === typeof data)
        ? data((_, data) => impl1(filepath, data))
        : impl1(filepath, data);
    return ('function' === typeof filepath)
      ? filepath((_, filepath) => impl2(filepath))
      : impl2(filepath);
  };

Now it is possible to mix simple and asynchronous values in arguments:

// creates test1.out file with "ok" in it
save('test1.out', 'ok')();

// copies test.in into test.out
write('test.out', read('test.in'))();

// prints content of the test.in file
read('test.in')((_, data) => console.log(data));

Now it is time to add an error handling. try/catch error handling in node.js is problematic due to asynchronous nature of operations[1]. In node.js the result of the asynchronous operation is either the error or value, returned in first or second argument of the callback function. When error occurs, the function should not continue the operations, instead it should just invoke the callback with error argument.

var read = filepath =>  
  callback => {
    let impl = filepath => {
      let options = { encoding : 'utf8' };
      fs.readFile(filepath, options, callback);
    };
    return ('function' === typeof filepath)
      ? filepath((err, filepath) =>
        err != null ? callback(err) : impl(filepath))
      : impl(filepath);
  };

var write = (filepath, data) =>
  callback => {
    let impl1 = (filepath, data) =>
      fs.writeFile(filepath, data, callback);
    let impl2 = filepath =>
      ('function' === typeof data)
        ? data((err, data) =>
          err != null ? callback(err) : impl1(filepath, data))
        : impl1(filepath, data);
    return ('function' === typeof filepath)
      ? filepath((err, filepath) =>
        err != null ? callback(err) : impl2(filepath))
      : impl2(filepath);
  };

Writing to the wrong path or reading from the file that does not exist results in callback invoked with error:

write('out', read('in'))((err, result) => console.log(err, result));
{ Error: ENOENT: no such file or directory, open 'in'
    at Error (native) errno: -2, code: 'ENOENT', syscall: 'open', path: 'in' } undefined

Notice that write is not creating the file in this example because the computation completes early due to the missing file 'in'.

This pattern can be applied to any asynchronous function with fixed number of parameters that completes by calling callback with error and value.


  1. Error Handling in Node.js, https://www.joyent.com/node-js/production/design/errors ↩︎

As previously mentioned, the converter from the asynchronous functions returning the result via callback can be created to support the simple asynchronous programming with node.js.

As the first step, the read method can be slightly modified to better separate implementation from wrapper:

const read = filepath =>  
  callback => {
    const impl = (filepath, callback) => {
      const options = { encoding : 'utf8' };
      fs.readFile(filepath, options, callback);
    };
    return ('function' === typeof filepath)
      ? filepath((err, filepath) =>
        err != null ? callback(err) : impl(filepath, callback))
      : impl(filepath, callback);
  };

Let's define fn1 function that gets implementation with value and callback and returns the wrapped function:

const fn = f => arg1 => callback => ('function' === typeof arg1)
  ? arg1((err, arg1) => err != null ? callback(err) : f(arg1, callback))
  : f(arg1, callback);

The read function can be replaced then by wrapping the implementation with fn:

const read = fn1((filepath, callback) => {
  const options = { encoding : 'utf8' };
  fs.readFile(filepath, options, callback);
});

Similarly, other functions of the fs module can be converted:

var exists = fn1(fs.exists);
var mkdir = fn1(fs.mkdir);
...

Now it is possible to use these async functions in a simple way:

// write the string 'test' to the file 'folder-to-create'
// and then read this file and create the folder 'test',
// print result to console.
write('folder-to-create', 'test')(() =>
  mkdir(read('folder-to-create'))(console.log));

Let's define the function fn that wraps any given node.js async callback function, which makes possible to call this function in the functional way:

const fn = f => (...args) => {
  return callback => {
    const fargs = args.map(_ => ({ value : undefined, set : false }));
    const set = (n, value) => fargs[n] = { value : value, set : true };
    const done = () => {
      if (fargs.every(item => item.set))
        f.apply(null, fargs.map(item => item.value).concat([callback]));
    };
    args.forEach((arg, i) => {
      if (isFn(arg)) {
        arg((err, val) => {
          if (err != null) {
            callback(err);
            return;
          }
          set(i, val);
          done();
        });
      } else {
        set(i, arg);
      }
    });
    done();
  }
};

And now let's try it with some of the fs functions:

var realpath = fn(fs.realpath);
var readdir = fn(fs.readdir);
readdir(realpath('.', 'utf8'))((err, result) => console.log(err, result));