In this article, we will learn how to write our implementation of util.promisify(). Before we could get started, there are two things which you must be aware of.
- What a callback hell is and how we could come out of it using promise. - Read more.
- Understanding the implementation of promises in javascript. - Read more.
Introduction
Promisification helps in dealing with callback-based APIs while keeping code consistent with promises. It converts a function that accepts a callback into a function that returns a promise.
Such transformations are often required in real life, as many functions and libraries are callback-based. Thus, promises are more convenient, so it makes sense to promisify them.
Let's dive into the code and understand this implementation.
//function defintion
const positiveSum = (num1, num2, callback) => {
if (num1<0 || num2<0) {
return callback(new Error("Negative numbers are not accepted"), null);
}
return callback(null, num1 + num2);
}
//function implementation
positiveSum(2, 3, (err, result) => {
if (err){
console.error(err);
} else {
console.log(result);
}
})
//output:
// 5
We define a function positiveSum
that accepts two positive numbers and a callback function. This callback function takes two parameters, error and result.
If either of the two numbers is negative, a new error object is created and passed as the first argument to the callback function. Otherwise, the callback function is returned with the summation of these two numbers that is passed as the second argument within the callback.
We can convert positiveSum
to return a promise using util.promisify()
as follows:
const util = require('util');
const calculateSum = util.promisify(positiveSum);
calculateSum(2,3).then((res) => {
console.log(res); // 5
}).catch((err) => {
console.error(err);
})
Creating our version of promisify
If we observe the above code snippet, promisify
accepts a callback as an argument.
Step1 - Creating myPromisify that accepts a callback
const calculateSum = myPromisify(positiveSum);
const myPromisify = (callback) => {
// implementation logic
}
We also observe that calculateSum
is taking two arguments, thus our implementation must return a function that takes two arguments.
Step 2 - Returning a function which could have multiple args
const myPromisify = (callback) => {
return function(...args){
// implmentation logic
}
}
In the above code, you can see that we are spreading arguments because we do not know how many arguments are present within the original function. args
will be an array containing all the arguments.
When you call calculate(2, 3) you’re actually calling (...args) => {}. In the implementation above it returns a promise. That’s why you’re able to use calculate(1, 1).then().catch().
Step 3 - Adding a promise to this returning function
const myPromisify = (callback) => {
return (...args) => {
return new Promise((resolve, reject) => {
})
}
}
Now, we would need to define when to call resolve and reject based on the callback that we pass to positiveSum
.
Step 4 - Implementing outcomes of a promise
const myPromisify = (fncallback) => {
return (...args) => {
return new Promise((resolve, reject) => {
function myCallback(err, result) {
if (err) {
reject(err)
}
else {
resolve(result);
}
}
})
}
}
Currently, args[]
consists of the arguments passed by calculateSum(2, 3)
and not the callback function. So we need to add myCallback(err, result)
to the args[]
so that positiveSum
will call it accordingly as we are tracking the result in myCallback
.
Step 5 - Adding myCallback
to args[]
const myPromisify = (callback) => {
return (...args) => {
return new Promise((resolve, reject) => {
function myCallback(err, result) {
if (err) {
reject(err)
}
else {
resolve(result);
}
}
args.push(myCallback)
callback.apply(this,args);
})
}
}
callback.apply(this, args)
will call the original function under the same context with the arguments positiveSum(2,3,myCallback)
. Then our promisify function should be able to resolve/reject accordingly.
Customize further
The above implementation will work when the original function expects a callback with two arguments, (err, result). That’s what we encounter most often. Then our custom callback is in the correct format and promisify works great for such a case.
But what if the original function expects a callback with more arguments like callback(err, result1, result2, ...)
?
In order to make it compatible with that, we need to modify our myPromisify function that will be an advanced version.
const myPromisify = (fn) => {
return (...args) => {
return new Promise((resolve, reject) => {
function customCallback(err, ...results) {
if (err) {
return reject(err)
}
return resolve(results.length === 1 ? results[0] : results)
}
args.push(customCallback)
fn.call(this, ...args)
})
}
}
Example: A function to calculate the sum and product of 2 numbers
const positiveSumAndProduct = (num1, num2, callback) => {
if (num1<0 || num2<0) {
return callback(new Error("Negative numbers are not accepted"), null);
}
const sum = num1 + num2;
const product = num1 * num2;
return callback(null, sum, product);
}
const mathPromise = myPromisify(positiveSumAndProduct)
mathPromise(2, 3).then(res => console.log(res))
//output
// [5, 6]
Hurray! we have successfully implemented our version of util.promisify
. Polyfills are one of the questions asked during interviews. I hope this article was helpful. Let me know your thoughts in the comments.
Reach out to me on Twitter if you have any queries. Happy learning! 💻
Peace ✌