Convert a Node.js callback API to a promise

Use util.promisify for error-first callbacks, preserve method context, and write a custom wrapper when a callback returns several values.

Best practices
Illustration accompanying a Node.js development guide

Use util.promisify() for a function whose final argument is a conventional error-first callback: (error, value) => .... It returns a new promise-based function, allowing await and ordinary try/catch handling.

Use the standard adapter

// read.cjs
const fs = require('node:fs');
const { promisify } = require('node:util');
const readFile = promisify(fs.readFile);

async function main() {
  const text = await readFile('config.json', 'utf8');
  console.log(text);
}
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

For modern file operations, prefer an existing promise API such as require('node:fs/promises').readFile. There is no need to adapt it again. Promisifying functions that already return promises is deprecated in recent Node releases.

Write a wrapper explicitly

A manual wrapper is useful when the callback contract differs:

const fs = require('node:fs');
function readText(path) {
  return new Promise((resolve, reject) => {
    fs.readFile(path, 'utf8', (error, text) => {
      if (error) {
        reject(error);
        return;
      }
      resolve(text);
    });
  });
}

Return after rejection so the error path does not continue into success handling. A synchronous exception thrown by the promise executor becomes a rejection. An exception thrown later inside an asynchronous callback is different: catch it there and call reject, or avoid throwing in that callback.

Preserve this for methods

If a method depends on its receiver, extracting it loses that context:

const { promisify } = require('node:util');
const store = {
  prefix: 'item:',
  get(id, callback) { callback(null, this.prefix + id); }
};
const get = promisify(store.get.bind(store));
get('42').then(console.log).catch(console.error);

Binding the original method before adapting it preserves this.

Keep multiple callback values

The ordinary adapter resolves one success value. For (error, value, metadata), define the result shape yourself:

const { promisify } = require('node:util');
function lookup(id, callback) {
  callback(null, { id }, { cached: false });
}
lookup[promisify.custom] = (id) => new Promise((resolve, reject) => {
  lookup(id, (error, value, metadata) => {
    if (error) return reject(error);
    resolve({ value, metadata });
  });
});
promisify(lookup)('42').then(console.log).catch(console.error);

Boundaries to check

Promisification does not modify the original function or make its work inherently faster. The returned wrapper has a promise interface; consumers of the original callback interface can continue using it.

A promise settles once. A stream, subscription, or repeated event source usually needs event handling or an async iterator instead of a wrapper that resolves on the first callback. Nonstandard APIs with separate success and failure callbacks also require a custom adapter. The filesystem promise namespace covers many operations, but streams remain stream APIs.

See util.promisify and file system promises.

Get a tailored plan

Contact us

Join the community

Scan with WeChat
to join the community

Try Guance

Start online and pay only for what you use.

Get started

Choose a Guance plan

Code hosting