JavaScript Promisification

Promisification is a long term for a straightforward transformation. It represents a conversion of the function, which accepts a callback into a function returning a promise.

Often, transformations like that are needed in real-life because multiple libraries and functions are based on callbacks. But, promises are much more convenient.

Let’s consider a load_Script(src, callback):

function load_Script(src, callback) {
  let scriptJS = document.createElement('script');
  scriptJS.src = src;
  scriptJS.onload = () => callback(null, scriptJS);
  scriptJS.onerror = () => callback(new Error(`Script load error for ${src}`));
  document.head.append(scriptJS);
}
// usage:
// load_Script('path/script.js', (err, scriptJS) => {...})

Now, we are going to promisify it. The new load_Script_Promise(src) will get the same result accepting merely src. It returns a promise, like this:

let load_Script_Promise = function (src) {
  return new Promise((resolve, reject) => {
    load_Script(src, (err, script) => {
      if (err) reject(err)
      else resolve(script);
    });
  })
}
// usage:
// load_Script_Promise('path/script.js').then(...)

So, it can be stated that ow load_Script_Promise suits well in a promise-based code. It delegates the overall work to the original load_Script and provides its callback, which translates to promise resolve/reject.

Now, it’s necessary to use a helper. It can be called promisify(fn). It includes a to-promisify function f, returning a wrapper function.

That wrapper does the same as the code, described above. It returns a promise and passes the call to the original fn.

The result is tracked in a custom callback, like here:

function promisify(fn) {
  return function (...args) { // return  wrapper function
    return new Promise((resolve, reject) => {
      function callback(err, result) { // our custom callback for fn
        if (err) {
          reject(err);
        } else {
          resolve(result);
        }
      }
      args.push(callback); // append custom callback at the end of the fn arguments 
      fn.call(this, ...args); // call the original function
    });
  };
};
// usage:
let load_Script_Promise = promisify(load_Script);
load_Script_Promise(...).then(...);

In the example above, the function waits for a callback along with two arguments such as result and err. So, when the callback is in the correct format, the promisify operates more efficiently.

Now, let’s discover a more complicated version of the promisify. After calling promisify(fn, true), the result of the promise will be an array of callback results, like this:

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

Also, there exist modules that have more flexible functions for promisification.

A built-in function, called util.promisify is used in Node.js.

Summary

Promisification is a useful and widespread approach that makes a developer’s life more productive. It is especially great while using it with async/await. However, the complete replacement for callbacks is not recommended.

It is important to note that a promise can have a single result. On the contrary, a callback can be called multiple times.

So, promisification is used for the functions, calling the callback once. Upcoming calls are always ignored.




Do you find this helpful?

Related articles