Skip to the code
cf.

Promisify

Write promisify(fn). fn is a callback-style function: it takes its arguments followed by a callback (error, result), as Node's fs.readFile does. promisify returns a new function that takes the same arguments without the callback, calls fn with them plus a callback of its own, and returns a promise.

The promise rejects with error when the callback gets one (any truthy error). Otherwise it fulfills with result, even when result is falsy, like 0 or "". If fn throws, the promise rejects with what it threw, so calling the new function never throws. Keep this, so methods can be promisified.

function readConfig(name, callback) {
  setTimeout(() => callback(null, { name, debug: true }), 100);
}
 
const readConfigAsync = promisify(readConfig);
const config = await readConfigAsync("app"); // { name: "app", debug: true }

What the tests check

  1. returns a function that returns a promise
    const ready = promisify((callback) => callback(null, "ok"));
    expect(typeof ready).toBe("function");
    const promise = ready();
    expect(promise).toBeInstanceOf(Promise);
    await promise;
    
  2. passes the arguments on, followed by a callback
    const fn = spy((a, b, callback) => callback(null, a + b));
    await promisify(fn)(2, 3);
    const args = fn.calls[0].args;
    expect(args).toHaveLength(3);
    expect(args.slice(0, 2)).toEqual([2, 3]);
    expect(typeof args[2]).toBe("function");
    
  3. fulfills with the result once the callback runs
    const clock = useFakeTimers();
    const read = promisify((name, callback) => {
      setTimeout(() => callback(null, "contents of " + name), 100);
    });
    let result;
    read("notes.txt").then((value) => {
      result = value;
    });
    await clock.tickAsync(99);
    expect(result).toBeUndefined();
    await clock.tickAsync(1);
    expect(result).toBe("contents of notes.txt");
    
  4. rejects with the error passed to the callback
    const error = new Error("ENOENT");
    const read = promisify((name, callback) => callback(error));
    await expect(read("missing.txt")).rejects.toBe(error);
    
  5. fulfills with falsy results too
    const zero = promisify((callback) => callback(null, 0));
    const empty = promisify((callback) => callback(undefined, ""));
    await expect(zero()).resolves.toBe(0);
    await expect(empty()).resolves.toBe("");
    
  6. keeps this, so methods work
    const db = {
      prefix: "user:",
      get: promisify(function (id, callback) {
        callback(null, this.prefix + id);
      }),
    };
    await expect(db.get(7)).resolves.toBe("user:7");
    
  7. rejects instead of throwing when fn throws
    const error = new TypeError("bad input");
    const parse = promisify(() => {
      throw error;
    });
    let promise;
    expect(() => {
      promise = parse("x");
    }).not.toThrow();
    await expect(promise).rejects.toBe(error);
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).