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.
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.
Contact us
Join the community
to join the community
Try Guance
Start online and pay only for what you use.
Get startedChoose a Guance plan