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.
// 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.
// 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.
(err, value). If err is truthy, reject with err; otherwise resolve with value. undefined and other falsy values for err mean success.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.try/catch so a synchronous throw from the original becomes a rejection rather than escaping to the caller.(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.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.