All questions

Promisify

Premium

Promisify

Implement Node's util.promisify. You take a function written in the error-first callback style — fn(arg1, arg2, ..., (err, value) => { ... }) — and return a new function with the same arguments minus the callback. Calling the new function returns a Promise that fulfils with value when the original callback gets (null, value) and rejects with err when it gets (err).

This is the bridge between two eras of JavaScript: the old callback-passing world and the modern async/await world. Once you've written promisify, every legacy callback API you touch becomes await-able.

Signature

// Wraps a Node-style (err, value) callback function and returns
// a function that returns a Promise instead.
function promisify(original: (...args: any[]) => void):
  (...args: any[]) => Promise<any>;

The original function must follow the convention: the last argument is a callback invoked as callback(err) on failure or callback(null, value) on success.

Examples

// fs.readFile-style API → awaitable.
function readFile(path, cb) {
  if (!path) cb(new Error('no path'));
  else cb(null, 'file contents');
}
const readFileAsync = promisify(readFile);

await readFileAsync('./hello.txt'); // 'file contents'
await readFileAsync('');            // throws Error('no path')
// Forwards all arguments to the original. Preserves `this`.
const api = {
  multiplier: 10,
  scale(n, cb) { cb(null, n * this.multiplier); },
};
api.scaleAsync = promisify(api.scale);
await api.scaleAsync.call(api, 4); // 40
// Multiple calls return independent promises.
const p1 = readFileAsync('a.txt');
const p2 = readFileAsync('b.txt');
// p1 and p2 settle independently of each other.

Notes

  • Error-first convention. The callback signature is (err, value). If err is truthy, reject with err; otherwise resolve with value. undefined and other falsy values for err mean success.
  • Forward every argument the caller passes to the wrapped function on to the original — including zero arguments. Use a rest parameter, not a fixed arity.
  • Preserve this. If the wrapped function is called as wrapped.call(ctx, ...) or as a method (obj.wrapped(...)), the original must see the same this. Arrow functions inside the wrapper will break this — use a regular function.
  • Single resolution. Wrap the call in try/catch so a synchronous throw from the original becomes a rejection rather than escaping to the caller.
  • No assumptions about callback shape beyond (err, value). If the original calls back with extra arguments (cb(null, a, b, c)), resolve with just the first value (a). The multi-value flavour is util.promisify's [promisify.custom] territory — out of scope here.
  • One promise per call. Each invocation of the wrapped function gets its own fresh new Promise(...). Don't memoise across calls.

Unlock the solution & editor

  • Runnable editor + tests

    Solve in the browser with instant Jest feedback.

  • Detailed solutions

    Walkthroughs, edge cases, and complexity notes.

  • Multi-framework variants

    React, Vue, Vanilla, Angular — same question, different stacks.

Upgrade to Premium