Promisification – is a long word for a simple transform. It’s conversion of a function that accepts a callback into a function returning a promise.

In other words, we create a wrapper-function that does the same, internally calling the original one, but returns a promise.

Such transforms are often needed in real-life, as many functions and libraries are callback-based. But promises are more convenient. So it makes sense to promisify those.

For instance, we have loadScript(src, callback) from the chapter Introduction: callbacks.

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(null, script);
  script.onerror = () => callback(new Error(`Script load error for ${src}`));

  document.head.append(script);
}

// usage:
// loadScript('path/script.js', (err, script) => {...})

Let’s promisify it. The new loadScriptPromise(src) function will do the same, but accept only src (no callback) and return a promise.

let loadScriptPromise = function(src) {
  return new Promise((resolve, reject) => {
    loadScript(src, (err, script) => {
      if (err) reject(err)
      else resolve(script);
    });
  })
}

// usage:
// loadScriptPromise('path/script.js').then(...)

Now loadScriptPromise fits well in our promise-based code.

As we can see, it delegates all the work to the original loadScript, providing its own callback that translates to promise resolve/reject.

As we may need to promisify many functions, it makes sense to use a helper.

That’s actually very simple – promisify(f) below takes a to-promisify function f and returns a wrapper function.

That wrapper does the same as in the code above: returns a promise and passes the call to the original f, tracking the result in a custom callback:

function promisify(f) {
  return function (...args) { // return a wrapper-function
    return new Promise((resolve, reject) => {
      function callback(err, result) { // our custom callback for f
        if (err) {
          return reject(err);
        } else {
          resolve(result);
        }
      }

      args.push(callback); // append our custom callback to the end of arguments

      f.call(this, ...args); // call the original function
    });
  };
};

// usage:
let loadScriptPromise = promisify(loadScript);
loadScriptPromise(...).then(...);

Here we assume that the original function expects a callback with two arguments (err, result). That’s what we meet most often. Then our custom callbacks is exactly in the right format, and promisify works great for such case.

But what if the original f expects a callback with more arguments callback(err, res1, res2)?

Here’s a modification of promisify that returns an array of multiple callback results:

// promisify(f, true) to get array of results
function promisify(f, manyArgs = false) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      function callback(err, ...results) { // our custom callback for f
        if (err) {
          return reject(err);
        } else {
          // resolve with all callback results if manyArgs is specified
          resolve(manyArgs ? results : results[0]);
        }
      }

      args.push(callback);

      f.call(this, ...args);
    });
  };
};

// usage:
f = promisify(f, true);
f(...).then(err => ..., arrayOfResults => ...)

In some cases, err may be absent at all: callback(result), or there’s something exotic in the callback format, then we can promisify such functions manually.

There are also modules with a bit more flexible promisification functions, e.g. es6-promisify. In Node.js, there’s a built-in util.promisify function for that.

Please note:

Promisification is a great approach, especially when you use async/await (see the next chapter), but not a total replacement for callbacks.

Remember, a promise may have only one result, but a callback may technically be called many times.

So promisification is only meant for functions that call the callback once. Furhter calls will be ignored.

Tutorial map

Comments

read this before commenting…
  • You're welcome to post additions, questions to the articles and answers to them.
  • To insert a few words of code, use the <code> tag, for several lines – use <pre>, for more than 10 lines – use a sandbox (plnkr, JSBin, codepen…)
  • If you can't understand something in the article – please elaborate.