Node.js 8: util.promisify()

[2017-05-30] dev, javascript, nodejs, esnext, async, promises
(Ad, please don’t block)

Node.js 8 has a new utility function: util.promisify(). It converts a callback-based function to a Promise-based one.

util.promisify() in action  

If you hand the path of a file to the following script, it prints its contents.

// echo.js

const {promisify} = require('util');

const fs = require('fs');
const readFileAsync = promisify(fs.readFile); // (A)

const filePath = process.argv[2];

readFileAsync(filePath, {encoding: 'utf8'})
  .then((text) => {
      console.log('CONTENT:', text);
  })
  .catch((err) => {
      console.log('ERROR:', err);
  });

Note how, in line A, the script uses promisify() to convert the callback-based function fs.readFile() to the Promise-based function readFileAsync().

The following interaction shows how the script is used:

$ node echo.js echo.js
CONTENT: const {promisify} = require('util');
···

$ node echo.js unknown.txt
ERROR: { Error: ENOENT: no such file or directory, ··· }

Using an async function  

The same functionality, but implemented via an async function:

// echoa.js

const {promisify} = require('util');

const fs = require('fs');
const readFileAsync = promisify(fs.readFile);

const filePath = process.argv[2];

async function main() {
    try {
        const text = await readFileAsync(filePath, {encoding: 'utf8'});
        console.log('CONTENT:', text);
    }
    catch (err) {
        console.log('ERROR:', err);
    }
}
main();

Promisifying functions whose callbacks have more than two parameters  

The callbacks of the following functions receive more than one result value (in addition to the error value):

  • child_process.exec
  • child_process.execFile
  • dns.lookup
  • dns.lookupService
  • fs.read
  • fs.write

If you promisify one of these functions, it returns an object of values (not a single value). For example, the callback of dns.lookup() has the following callback parameters:

  • err : Error
  • address : string
  • family : integer

Its promisified version fulfills its Promise with an {address, family} object:

const util = require('util');
const dns = require('dns');
const lookupAsync = util.promisify(dns.lookup);

lookupAsync('nodejs.org')
    .then(obj => console.log(obj));
    // { address: '104.20.23.46', family: 4 }

promisify() handles non-standard callbacks via the internal symbol internal/util/customPromisifyArgs. Given that non-standard callbacks are not recommended, you should never need this mechanism for your own functions.

Custom promisified functions  

The API of promisified comes with the symbol util.promisify.custom, which lets you attach a promisified version to a callback-based function. In the following example, fooAsync is the promisified version of foo.

const util = require('util');

function foo() {
    return 'abc';
}
async function fooAsync() {
    return 'abc';
}
foo[util.promisify.custom] = fooAsync;

console.log(util.promisify(foo) === fooAsync); // true

Standard functions with custom promisified versions  

At the moment, two standard functions have promisified versions:

> setImmediate[util.promisify.custom]
[Function]
> setTimeout[util.promisify.custom]
[Function]

promisify() needs help here, because these functions have the callback as the first parameter – against Node.js conventions:

A polyfill for older versions of Node.js  

Jordan Harband has created a polyfill for util.promisify(). It works as follows.

Requirements:

  • Engine must support at least ES5
  • A global Promise constructor
  • Warning: still work in progress

Installation:

npm install util.promisify

There are two main ways of using this polyfill.

First: retrieve either the built-in implementation (Node 8) or a polyfill (older versions of Node).

const promisify = require('util.promisify');

const fs = require('fs');
const readFileAsync = promisify(fs.readFile);

Second: patch module util on older versions of Node.

const util = require('util');
require('util.promisify').shim();

const fs = require('fs');
const readFileAsync = util.promisify(fs.readFile);

Further reading