Skip to the code
cf.

promisifyAll

Write promisifyAll(obj). For every method of obj, it adds a promise-returning version named after it with Async on the end: get gets a getAsync. It returns obj itself, and the original methods keep working as before.

The methods take a Node-style callback: their arguments, then a callback (error, result). Each Async version takes the same arguments without the callback and returns a promise. The promise rejects with error when the callback gets a truthy one, and fulfills with result otherwise, even when result is falsy, like 0. If the method throws, the promise rejects with what it threw.

Calling obj.getAsync(…) runs get with this set to obj, so methods that use this keep working.

A method is any property of obj whose value is a function, enumerable or not: its own, and those it inherits through its prototype chain, such as the methods of its class and of the classes that class extends. constructor and the methods of Object.prototype, like toString, don't count, and neither do properties that aren't functions.

When a subclass overrides a method, getAsync uses the subclass's version: the one obj.get gives you.

class Store {
  constructor() {
    this.items = new Map();
  }
  get(key, callback) {
    setTimeout(() => {
      if (this.items.has(key)) callback(null, this.items.get(key));
      else callback(new Error("No such key: " + key));
    }, 10);
  }
  set(key, value, callback) {
    this.items.set(key, value);
    setTimeout(() => callback(null), 10);
  }
}
 
const store = promisifyAll(new Store());
await store.setAsync("theme", "dark"); // undefined
await store.getAsync("theme"); // "dark"
await store.getAsync("font"); // rejects with Error: No such key: font
typeof store.itemsAsync; // "undefined": items isn't a method

What the tests check

  1. returns the same object
    const obj = {
      read(callback) {
        callback(null, "data");
      },
    };
    expect(promisifyAll(obj)).toBe(obj);
    
  2. adds a promise-returning nameAsync for each method
    const api = promisifyAll({
      read(callback) {
        callback(null, "data");
      },
      write(text, callback) {
        callback(null, text.length);
      },
    });
    const reading = api.readAsync();
    expect(reading).toBeInstanceOf(Promise);
    await expect(reading).resolves.toBe("data");
    await expect(api.writeAsync("hello")).resolves.toBe(5);
    
  3. waits for the callback, however long it takes
    const clock = useFakeTimers();
    const api = promisifyAll({
      read(name, callback) {
        setTimeout(() => callback(null, "contents of " + name), 100);
      },
    });
    let result;
    api.readAsync("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. passes the arguments on, followed by a callback, with the object as this
    const add = spy((a, b, callback) => callback(null, a + b));
    const api = promisifyAll({ add });
    await expect(api.addAsync(2, 3)).resolves.toBe(5);
    const call = add.calls[0];
    expect(call.args).toHaveLength(3);
    expect(call.args.slice(0, 2)).toEqual([2, 3]);
    expect(typeof call.args[2]).toBe("function");
    expect(call.this).toBe(api);
    
  5. rejects only when the callback gets an error
    const error = new Error("disk full");
    const api = promisifyAll({
      save(callback) {
        callback(error);
      },
      count(callback) {
        callback(null, 0);
      },
    });
    await expect(api.saveAsync()).rejects.toBe(error);
    await expect(api.countAsync()).resolves.toBe(0);
    
  6. rejects instead of throwing when the method throws
    const error = new TypeError("bad input");
    const api = promisifyAll({
      parse() {
        throw error;
      },
    });
    let promise;
    expect(() => {
      promise = api.parseAsync("x");
    }).not.toThrow();
    await expect(promise).rejects.toBe(error);
    
  7. includes the methods it inherits from classes
    class Connection {
      ping(callback) {
        callback(null, "pong");
      }
    }
    class Database extends Connection {
      constructor() {
        super();
        this.rows = { 1: "Ada" };
      }
      find(id, callback) {
        callback(null, this.rows[id]);
      }
    }
    const db = promisifyAll(new Database());
    await expect(db.findAsync(1)).resolves.toBe("Ada");
    await expect(db.pingAsync()).resolves.toBe("pong");
    
  8. uses the subclass's version of an overridden method
    class Base {
      describe(callback) {
        callback(null, "base");
      }
    }
    class Child extends Base {
      describe(callback) {
        callback(null, "child");
      }
    }
    const child = promisifyAll(new Child());
    await expect(child.describeAsync()).resolves.toBe("child");
    
  9. skips constructor, Object.prototype's methods, and properties that aren't functions
    class Service {
      constructor() {
        this.retries = 3;
      }
      start(callback) {
        callback(null);
      }
    }
    const service = promisifyAll(new Service());
    expect(typeof service.startAsync).toBe("function");
    expect(service.constructorAsync).toBeUndefined();
    expect(service.toStringAsync).toBeUndefined();
    expect(service.hasOwnPropertyAsync).toBeUndefined();
    expect(service.retriesAsync).toBeUndefined();
    

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).