Bridging Callbacks and Promises
promisify, callbackify, and the custom-promisify escape hatch for non-standard APIs.
Plenty of libraries — and older Node APIs — still speak error-first callbacks. util.promisify wraps any function that follows the (err, value) convention into one that returns a promise, so it slots into async/await.
const util = require('node:util');
const { execFile } = require('node:child_process');
const run = util.promisify(execFile);
const { stdout } = await run('node', ['--version']);
console.log(stdout.trim());When the callback returns more than one value
Some callbacks yield multiple results, e.g. (err, stdout, stderr). A plain promise resolves a single value, so those APIs expose a util.promisify.custom symbol that promisify honors, returning an object instead. This is why promisify(execFile) resolves to { stdout, stderr } rather than just stdout.
The other direction
util.callbackify turns an async function back into an error-first callback function — handy when you must plug modern code into an old callback-based framework.
Example
// Teaching promisify how to handle a non-standard callback shape via
// the custom symbol, and building a resilient sleep with timers/promises.
const util = require('node:util');
const { setTimeout: sleep } = require('node:timers/promises');
// A legacy API whose callback returns (err, value, meta) — two data args.
function legacyFetch(id, cb) {
setTimeout(() => cb(null, { id, name: 'row' }, { cached: false }), 10);
}
// Attach a custom promisified implementation so we keep BOTH data args.
legacyFetch[util.promisify.custom] = (id) =>
new Promise((resolve, reject) => {
legacyFetch(id, (err, value, meta) =>
err ? reject(err) : resolve({ value, meta }));
});
const fetchRow = util.promisify(legacyFetch);
const { value, meta } = await fetchRow(42);
console.log(value, meta); // { id: 42, name: 'row' } { cached: false }
// timers/promises gives you awaitable delays without wrapping setTimeout.
await sleep(25);
console.log('resumed after 25ms');When to use it
- A developer converts the legacy `dns.lookup` callback API to a promise using `util.promisify` so it can be awaited in modern async functions.
- A custom third-party SDK that only exposes callbacks is wrapped with `util.promisify` so the entire codebase stays consistently promise-based.
- A test helper wraps `setTimeout` with `util.promisify` to create a `sleep(ms)` utility that integrates cleanly with async test flows.
More examples
util.promisify a callback API
Converts `fs.readFile`'s callback signature into a Promise-returning function using `util.promisify`.
const { promisify } = require('util');
const fs = require('fs');
const readFile = promisify(fs.readFile);
async function main() {
const data = await readFile('notes.txt', 'utf8');
console.log(data);
}
main();Promisify setTimeout as sleep
Uses `util.promisify` on `setTimeout` to create a clean `sleep(ms)` helper for async code.
const { promisify } = require('util');
const sleep = promisify(setTimeout);
async function main() {
console.log('Starting...');
await sleep(1000); // waits 1 second
console.log('Done after 1s');
}
main();util.promisify.custom symbol
Uses `promisify.custom` to attach an optimized promise implementation that `util.promisify` will prefer.
const { promisify } = require('util');
function fetchData(id, callback) {
callback(null, { id, name: 'Widget' });
}
// Attach a custom promisified version
fetchData[promisify.custom] = (id) =>
Promise.resolve({ id, name: 'Widget' });
const fetchDataAsync = promisify(fetchData);
fetchDataAsync(42).then(console.log);
Discussion