promisify
Prompt
Implement a promisify function that takes a callback-based function and returns a new function that returns a Promise instead.
The original function follows the Node.js error-first callback pattern: its last argument is a callback of the form (error, result). Your promisified version should reject with the error or resolve with the result.
Playground
promisify returns a new function. That function, when
called, should return a new Promise. Inside the promise,
call the original function with all the arguments the
caller passed, plus one extra argument at the end: your
own callback.
The callback you attach should follow the error-first pattern: (err, result) => { ... }. If err exists, call reject(err). Otherwise, call resolve(result).
Use ...args to capture whatever arguments the caller
passes, then call the original function as fn(...args, callback). This way it works regardless of how many
arguments the original function expects.
Solution
Explanation
Before Promises existed, Node.js used callbacks for everything. Functions like fs.readFile(path, callback) expected you to pass a callback as the last argument, and that callback always followed the same pattern: first argument is the error (or null if no error), second argument is the result.
getUserData(1, (err, user) => {
if (err) {
console.error(err);
return;
}
console.log(user);
});promisify bridges this old callback world with the modern Promise/async-await world. It takes a callback-based function and returns a new function that returns a Promise instead.
How it works
function promisify(fn) {
return function (...args) {
return new Promise((resolve, reject) => {
fn(...args, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
};
}There are three layers here, and each one has a specific job:
Layer 1: promisify(fn) receives the original callback-based function and remembers it. It doesn't call it yet. It just returns a new wrapper function that we'll use instead.
Layer 2: function (...args) is the wrapper function. This is what the caller actually uses. When someone calls getUser(1), the 1 gets captured into args as [1]. This wrapper returns a new Promise, which is how we convert from callbacks to promises.
Layer 3: Inside the Promise, we finally call the original function. We pass it all the caller's arguments (...args) plus one extra argument at the end: our own callback. So fn(...args, callback) becomes getUserData(1, callback), which is exactly what getUserData expects.
Our callback checks: did the original function report an error? If err is truthy, we reject the promise. If not, we resolve with the result. That's it. The error-first callback pattern maps perfectly to reject/resolve.
Before and after
// Before: callback style
getUserData(1, (err, user) => {
if (err) return console.error(err);
console.log(user);
});
// After: promise style
const getUser = promisify(getUserData);
getUser(1)
.then((user) => console.log(user))
.catch((err) => console.error(err));
// Even better: async/await
const user = await getUser(1);The promisified version is cleaner, chains better, and works with async/await. That's why Node.js itself ships with util.promisify built in since version 8.