How to promisify a function in Node.js

Jun 30, 2023#node#how-to

Promisifying a function in Node.js, aslo known as “promisification”, involves converting a callback-based function into a function that returns a Promise. This allows you to use the more modern and convenient async/await syntax or promise chaning when working with asynchronous operations.

Using util.promisify

Node.js provides a utility module called util that includes a promisify method for easily converting callback-based functions to promises. Here’s an example:

const fs = require('fs');
const util = require('util');

// Convert `fs.readFile()` into a function that returns a promise
const readFile = util.promisify(fs.readFile);

// You can now use `readFile()` with `await`!
const buf = await readFile('./package.json');
const obj = JSON.parse(buf.toString('utf8'));
console.log(obj.name) // byby.dev

Method util.promisify works with functions that have a callback as the last argument, and the callback follows the Node.js convention of having an error as the first argument and a single result as the second argument (err, value) => .... For example:

function add(a, b, callback) {
  // do some calculation
  let result = a + b;
  // pass null as the error and result as the second argument
  callback(null, result);
}

If you have a function that has more than one argument after the callback, or the callback does not follow the Node.js convention, you can use one of the following options:

  • Using util.promisify.custom symbol
  • Write your own custom promisify function
  • Wrap into a Promise-based function
  • Using third-party libraries

Using util.promisify.custom

The symbol util.promisify.custom is defined by the util library in Node.js. It allows you to attach a custom promisified version to a callback-based function that does not follow the Node.js convention of having an error-first callback as the last argument. For example, if you have a function like this:

function timer(callback, t, v) {
  // do something after t milliseconds and pass v and err to callback
  callback(v, err);
}

You can make it compatible with util.promisify by adding a property with the symbol util.promisify.custom and assigning it a function that returns a promise:

timer[util.promisify.custom] = (t, v) => {
  return new Promise((resolve, reject) => {
    timer((value, err) => {
      if (err) {
        reject(err);
      } else {
        resolve(value);
      }
    }, t, v);
  });
};

Then you can use util.promisify to get a promise-based version of timer:

const timerAsync = util.promisify(timer);
timerAsync(1000, 'hello').then(console.log).catch(console.error);

Write your own promisify function

If you prefer a manual approach or if you are using an older version of Node.js that doesn’t have util.promisify, you can manually create a wrapper function that returns a promise. Here’s an example:

function promisify(callbackBasedFunction) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      callbackBasedFunction(...args, (error, result) => {
        if (error) {
          reject(error);
        } else {
          resolve(result);
        }
      });
    });
  };
}

// Assuming you have a function called 'callbackFunction'
const promisifiedFunction = promisify(callbackFunction);

There are different ways to implement promisify by yourself, depending on the function you want to convert and the features you need. You have complete control over the behavior and implementation. This can be valuable if you’re developing cross-platform applications or libraries that need to work in various contexts.

Wrap into a promise-based function

If you want to manually wrap a callback-based function into a Promise-based function or an async function without using util.promisify, you can create a function that returns a new Promise and handles the asynchronous operations and callback internally.

// Example callback-based function
function callbackBasedFunction(arg1, arg2, callback) {
  // Do some asynchronous operations
  // Call the callback with the result or error
}

// Promise-based function wrapper
function promisedFunction(arg1, arg2) {
  return new Promise((resolve, reject) => {
    callbackBasedFunction(arg1, arg2, (error, result) => {
      if (error) {
        reject(error); // Reject the Promise with the error
      } else {
        resolve(result); // Resolve the Promise with the result
      }
    });
  });
}

// Async function wrapper
async function asyncFunction(arg1, arg2) {
  return await promisedFunction(arg1, arg2);
}

You’ll have full control over promise-based function arguments and behavior. Since new Promise() is a core feature of the JavaScript language and is supported across different environments, including browsers and Node.js, it provides a portable solution that doesn’t rely on any specific library or module.

Manually wrapping the function requires duplicating the code for handling the asynchronous operations and callback. This can lead to code duplication if you have multiple callback-based functions that need to be wrapped.

This method requires more manual effort and attention to handle errors, context, and Promise chaining. Therefore, if util.promisify is available, it is generally recommended to use it for a simpler and more streamlined approach.

Using third-party libraries

Use a third-party library like Pify, which supports wrapping a whole module/object, not just a specific method, and useful options like the ability to handle multiple arguments.

import fs from 'fs';
import pify from 'pify';
import request from 'request';

// Promisify a single function.
const data = await pify(fs.readFile)('package.json', 'utf8');
console.log(JSON.parse(data).name);
//=> 'pify'

// Promisify all methods in a module.
const data2 = await pify(fs).readFile('package.json', 'utf8');
console.log(JSON.parse(data2).name);
//=> 'pify'

const pRequest = pify(request, {multiArgs: true});
const [httpResponse, body] = await pRequest('https://example.com');

or es6-promisify, a package that converts callback-based functions to ES6 promises. It is similar to the built-in util.promisify function from Node.js, but it works in browsers and other environments that support ES6.

const {promisify} = require("es6-promisify");

// Convert the stat function
const fs = require("fs");
const stat = promisify(fs.stat);

// Now usable as a promise!
try {
    const stats = await stat("example.txt");
    console.log("Got stats", stats);
} catch (err) {
    console.error("Yikes!", err);
}

Caveats of util.promisify

Using util.promisify can be very useful and convenient, but it also has some caveats that you should be aware of. Here are some of them:

  • Error handling: When using util.promisify, it assumes that the callback follows the Node.js convention where the first parameter is reserved for an error. If the callback doesn’t adhere to this convention, the resulting Promise will be resolved with the first non-error argument, which may not be desirable. Additionally, exceptions thrown inside the promisified function will not be caught unless you explicitly handle them.

  • Context and “this”: util.promisify does not automatically bind the this context for the original function. If the function relies on accessing this within its implementation, special handling is required to ensure that the context is preserved correctly. You may need to use bind() or arrow functions to maintain the desired context.

  • Multiple arguments: The original callback-based function may pass multiple arguments to the callback, while Promises typically resolve with a single value. When using util.promisify, only the first argument passed to the callback will be resolved by the Promise. If you need to access multiple values, you may need to modify the promisified function to return an object or an array containing the desired values.

  • Asynchronous function types: util.promisify is primarily designed for converting traditional Node.js-style callback functions that follow the (error, result) signature. It may not work correctly with functions that have different conventions or behavior, such as functions returning multiple callbacks or functions that rely on complex control flow. In such cases, manual adaptation or custom promisification may be necessary.

  • Compatibility: util.promisify is part of the util module in Node.js, so it’s not available in other JavaScript environments by default. You’ll need to provide your own implementation or use a library that offers similar functionality.

Overall, while util.promisify is a handy tool for simplifying asynchronous code, it’s important to understand its limitations and potential caveats to ensure proper usage in different scenarios.