Implement promisify

Promisify converts an error-first callback function into one that returns a promise. Return a wrapper that calls the original with the same arguments plus a callback of your own, rejecting when the first argument is an error and resolving with the second otherwise. The wrapper must be a regular function so the caller's this survives.

Easy to hard promises callbacks this binding

The question

Usually posed as "turn fs.readFile into something you can await". The interesting half is the follow-up: what happens when the function being promisified is a method that depends on its object?

The solution

The wrapper is deliberately a regular function, not an arrow. That single choice is what the two hard tests exist to check.

function promisify(func) {
  // A regular function, not an arrow, so the caller's this reaches func.
  return function (...args) {
    return new Promise((resolve, reject) => {
      func.call(this, ...args, (error, value) => {
        if (error) reject(error);
        else resolve(value);
      });
    });
  };
}

How it works

The wrapper is a regular function, not an arrow, so this is whatever the caller used. func.call(this, ...args, callback) forwards the receiver and appends the callback last, where the error-first convention expects it. A non-null first argument rejects, the second resolves, and the promise is built inside the wrapper so each call gets its own.

The tests it has to pass

This is the part most explanations leave out. "It works" is not a claim you can check by reading. These are the 5 cases Practice Pad runs against your solution, and the implementation above passes all of them.

Easy3 cases · values and arguments

fulfils with the value the callback produces

function doubleLater(value, callback) {
  setTimeout(() => { callback(null, value * 2); }, 20);
}
expectEqual(await promisify(doubleLater)(6), 12);

passes every argument through to the wrapped function

function joinLater(a, b, callback) {
  setTimeout(() => { callback(null, a + "-" + b); }, 10);
}
expectEqual(await promisify(joinLater)("x", "y"), "x-y");

returns a promise rather than the callback result

function readLater(callback) {
  setTimeout(() => { callback(null, "value"); }, 10);
}
const pending = promisify(readLater)();
expectEqual(pending instanceof Promise, true);
expectEqual(await pending, "value");
Hard2 cases · preserving the receiver

uses the object supplied by its caller

const multiplier = {
  factor: 3,
  scaleLater(value, callback) {
    setTimeout(() => { callback(null, value * this.factor); }, 20);
  },
};
const scaleAsync = promisify(multiplier.scaleLater);
expectEqual(await scaleAsync.call(multiplier, 7), 21);

keeps the caller object across repeated calls

const counter = {
  step: 5,
  addLater(value, callback) {
    setTimeout(() => { callback(null, value + this.step); }, 10);
  },
};
const addAsync = promisify(counter.addLater);
expectEqual([await addAsync.call(counter, 1), await addAsync.call(counter, 10)], [6, 15]);

Edge cases to watch out for

Returning an arrow function

The most common failure, and the reason both hard tests exist. An arrow has no this, so promisify(obj.method).call(obj, x) loses the object and the method reads undefined properties.

Calling func(...args, callback) instead of func.call(this, ...)

Even with a regular wrapper, invoking the original as a bare function discards the receiver. The forwarding has to be explicit.

Creating the promise outside the wrapper

A promise built once at promisify time settles once and returns the same result to every caller. It must be created per call.

Testing the error against null instead of truthiness

Some callback APIs pass undefined rather than null for success. if (error) handles both; if (error !== null) rejects every successful call from those APIs.

Node's version does more. util.promisify honours a util.promisify.custom symbol so libraries can supply their own implementation, and handles callbacks that yield several values. Neither is usually asked for, but knowing they exist is a good closing remark.

Follow up questions

What is an error-first callback?

The Node convention where a callback receives the error as its first argument and the result as its second, so callback(null, value) on success and callback(error) on failure. Promisify exists to translate that convention into promise rejection and resolution.

How do you convert a promise back into a callback?

The other direction, sometimes called callbackify: call the function, then .then(value => callback(null, value), error => callback(error)). Node ships util.callbackify for it. Useful when a promise-based implementation has to be handed to an older API that still expects an error-first callback.

How do you promisify a callback that returns multiple values?

Resolve with an array or object of them: (error, a, b) => error ? reject(error) : resolve([a, b]). Node's util.promisify supports this through the util.promisify.custom symbol, since the default only ever passes the first value through.

Practise this with the tests running

Practice Pad runs these 5 cases against your own solution, in a real editor, on your Mac. Twenty-four questions, and an optional AI panel that explains why a test failed.

Download for macOS Free. Requires macOS 12 or later on Apple Silicon.

See all 24 questions