en.javascript.info/1-js/11-async/06-promisify/article.md
2019-03-13 23:08:35 +03:00

4.1 KiB

Promisification

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 info: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)
      elsee 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 is 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.

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.