The `util.promisify()` Function in Node.js

Feb 17, 2020

Node.js' built-in util package has a promisify() function that converts callback-based functions to promise-based functions. This lets you use promise chaining and async/await with callback-based APIs.

For example, Node.js' fs package uses callbacks. Normally, to read a file, you would need to use callbacks:

const fs = require('fs');

fs.readFile('./package.json', function callback(err, buf) {
  const obj = JSON.parse(buf.toString('utf8'));
  obj.name; // 'masteringjs.io'
});

You can use util.promisify() to convert the fs.readFile() function to a function that returns a callback:

const fs = require('fs');
const util = require('util');

// Convert `fs.readFile()` into a function that takes the
// same parameters but returns a promise.
const readFile = util.promisify(fs.readFile);

// You can now use `readFile()` with `await`!
const buf = await readFile('./package.json');

const obj = JSON.parse(buf.toString('utf8'));
obj.name; // 'masteringjs.io'

Assumptions

How does util.promisify() work under the hood? There's a polyfill on npm, you can read the full implementation here.

The key idea behind util.promisify() is that it adds a callback function to the parameters you passed in. That callback function resolves or rejects the promise the promisified function returns.

That's a bit of a mouthful, so here's a very simplified example of a custom implementation of util.promisify().

const fs = require('fs');

// A simplified implementation of `util.promisify()`. Doesn't
// cover all cases, don't use this in prod!
function promisify(fn) {
  return function() {
    const args = Array.prototype.slice.call(arguments);
    return new Promise((resolve, reject) => {
      fn.apply(this, [].concat(args).concat([(err, res) => {
        if (err != null) {
          return reject(err);
        }
        resolve(res);
      }]));
    });
  };
}

// Convert `fs.readFile()` into a function that takes the
// same parameters but returns a promise.
const readFile = promisify(fs.readFile);

// You can now use `readFile()` with `await`!
const buf = await readFile('./package.json');

const obj = JSON.parse(buf.toString('utf8'));
obj.name; // 'masteringjs.io'

So what does this mean? First, util.promisify() adds 1 extra argument to the arguments you passed in, and then calls the original function with those new arguments. That means the underlying function needs to support that number of arguments. So if you're calling a promisified function myFn() with 2 parameters of types [String, Object], make sure the original function supports a call signature of [String, Object, Function].

Secondly, util.promisify() has implications for function context.

Losing Context

Losing context means that a function call ends up with the wrong value of this. Losing context is a common problem for transformed functions:

class MyClass {
  myCallbackFn(cb) {
    cb(null, this);
  }
}

const obj = new MyClass();
const promisified = require('util').promisify(obj.myCallbackFn);

const context = await promisified();
context; // `undefined` instead of a `MyClass` instance!

Remember that this contains whatever object the function was a property of when it was called. So you can retain context by setting the promisified function as a property of the same object:

class MyClass {
  myCallbackFn(cb) {
    cb(null, this);
  }
}

const obj = new MyClass();
// Retain context because `promisified` is a property of `obj`
obj.promisified = require('util').promisify(obj.myCallbackFn);

const context = await obj.promisified();
context === obj; // true

More Node Tutorials