You are here

Promisify A Callback Function Without 3rd Party Libraries

Corey Pennycuff's picture

I was recently working on some code in which I wanted to use Promises, but all that I had was callback-centric code. In the past, I have imported libraries such as bluebird for this particular problem, but now that seems like a bit of overkill to get just one function. I'm already using ES6, so I don't need a polyfill in order to use Promises; all that I want is a simple promisify() function. Importing an entire library for one function is, IMO, wasteful. Besides, it's more fun to write (and therefore understand at a deeper level) your own implementation, so that's what I did.

If you don't care about why this code works, then you can see it in its completed form below. If you are actually curious as to how I came up with it, then read on and I will explain it to you. The finished code is as follows:

/**
 * Convert a callback-based function into a promise.
 **/
function promisify(fn) {
  return function() {
    let args = Array.from(arguments);
    return new Promise((resolve, reject) => fn(...args, (error, content) => error ? reject(error) : resolve(content)));
  };
}

First of all, we should describe the coding situation. In Node.js (and many related JavaScript libraries), most callback functions use the same form, as shown below with our fictional foo() function:

function foo([arg1, arg2, arg3,] callback);

The callback() function will be called when the foo() function is finished. What we want, rather, is to be able to use this function as though it were a Promise.

let bar = promisify(foo);
bar(arg1, arg2, arg3).then(callback);
bar(differentArg1, differentArg2, differentArg3).then(differentCallback);

By defining how we expect to use promisify() and bar(), we can infer the internal structure of the promisify() function. Notice that bar is a function, so promisify() must return a function. Here is the skeleton:

function promisify(fn) {
  return function bar() {};
}

Here we run into our first snag. When bar() is called, we need to pass its arguments into fn (the function that we originally called promisify() on).

function promisify(fn) {
  return function bar() {
    fn.apply(undefined, arguments);
  };
}

This will not work, however, because fn expects the last element in the array to be a callback. Let's add a dummy callback function and include it in the .apply() function arguments array.

function promisify(fn) {
  return function bar() {
    let args = Array.from(arguments);
    args.push(function callback(err, data) {});
    fn.apply(undefined, args);
  };
}

Notice that we are not returning anything, however, from bar(), and this is incorrect according to our use case. bar() should return a Promise!

function promisify(fn) {
  return function bar() {
    let args = Array.from(arguments);
    args.push(function callback(err, data) {});
    fn.apply(undefined, args);
    return new Promise(function(resolve, reject) {});
  };
}

Now bar() returns a Promise, but we want to be able to put the callback into the .then() of the Promise returned by bar(). We can do this by moving the fn.apply() call inside the Promise code. We still need our generic callback() function, but now we need it to call either the reject() or resolve() function based on the values in the err and data variables, respectively.

/**
 * Convert a callback-based function into a promise.
 * Verbose version!
 **/
function promisify(fn) {
  return function bar() {
    let args = Array.from(arguments);
    return new Promise(function(resolve, reject) {
      args.push(function callback(err, data) {
        if (err) {
          reject(err);
        }
        else {
          resolve(data);
        }
      });
      fn.apply(undefined, args);
    });
  };
}

The version just shown to you is correct, and will function as intended, but it is a bit verbose. After all, we are using ES6, and we aren't using very many of the language features. Let's begin condensing some of the code, starting with replacing the if() statement with a ternary operator.

function promisify(fn) {
  return function bar() {
    let args = Array.from(arguments);
    return new Promise(function(resolve, reject) {
      args.push(function callback(err, data) {
        err ? reject(err) : resolve(data);
      });
      fn.apply(undefined, args);
    });
  };
}

Next, we are going to use anonymous functions rather than giving them names. We can also use the ES6-style () => {} function declarations everywhere except for the return of promisify().

function promisify(fn) {
  return function() {
    let args = Array.from(arguments);
    return new Promise((resolve, reject) => {
      args.push((err, data) => {
        err ? reject(err) : resolve(data);
      });
      fn.apply(undefined, args);
    });
  };
}

Let's get rid of the .push() and .apply() by using the spread syntax. Notice that we cannot avoid the need to copy the arguments array, because the arguments array needs to be used inside another function, so we must save a copy.

function promisify(fn) {
  return function() {
    let args = Array.from(arguments);
    return new Promise((resolve, reject) => {
      fn(...args, (err, data) => {
        err ? reject(err) : resolve(data);
      });
    });
  };
}

The next optimization is not as easy to spot, but notice that there are two functions that only have one line of code within them, and no return statement. In this particular case, it does not matter if anything is returned by the functions or not, because the value is immediately discarded. This fact allows us to further shorten the anonymous functions. I will do them one at a time, the innermost function being first.

function promisify(fn) {
  return function() {
    let args = Array.from(arguments);
    return new Promise((resolve, reject) => {
      fn(...args, (err, data) => err ? reject(err) : resolve(data));
    });
  };
}

Now to compress it a bit more.

/**
 * Convert a callback-based function into a promise.
 **/
function promisify(fn) {
  return function() {
    let args = Array.from(arguments);
    return new Promise((resolve, reject) => fn(...args, (err, data) => err ? reject(err) : resolve(data)));
  };
}

There you have it! We just created a function that, given a callback-style function, returns a Promise-based implementation of it. If you really, really want a short version, go for this:

/**
 * Convert a callback-based function into a promise.
 * Ultra-condensed version.
 **/
let promisify = fn => function() {
  let args = Array.from(arguments);
  return new Promise((resolve, reject) => fn(...args, (err, data) => err ? reject(err) : resolve(data)));
};

Of course I didn't do anything with try..catch blocks, but you can add them as you see fit. From this point, it would also be trivial to support callback-based functions who put their callback in non-standard locations (e.g., putting the callback as the first argument or in the middle of the arguments, followed by optional arguments, etc.).

Tags: