Node.js, part 2: util.promisify and living with callbacks
Part 2from the Node.js series · 24 parts in all
Everything in Node's standard library was a callback API first, and the ecosystem is full
of third-party modules written the same way. You do not have to wrap each one by hand:
util.promisify converts a Node-style function — one whose last argument is
(err, value) — into a promise-returning one. Part 2 is what it can do and, more
usefully, where it silently loses information.
One line per function
const util = require('util');
const fs = require('fs');
const readFile = util.promisify(fs.readFile);
const text = await readFile('package.json', 'utf8');
// Works on your own functions too, as long as the shape is right.
const wait = (ms, cb) => setTimeout(cb, ms);
const sleep = util.promisify(wait);
await sleep(500);
The convention it depends on is exact: the callback is the last argument, its first parameter is the error, and it is called once. Anything that calls its callback twice (some libraries do, on retry) will leave a stray resolution you cannot see.
When promisify is the wrong tool
Two shapes break it, and both are worth recognising before you spend an afternoon debugging:
- A callback with more than one value.
promisifyresolves with the first only, so anything returning two results loses the second. Wrap it by hand:new Promise((resolve) => fn((err, a, b) => resolve({ err, a, b }))). - A function that is not a callback at all. If the "callback" is an event
listener (
on('data')) or a stream, promisify has nothing to hook. That is what streams' own helpers are for, which is part 3.
The escape hatch for library authors
const util = require('util');
function query(sql, cb) { /* ... */ }
// Publish a hand-written promise version and promisify will prefer it,
// so callers get exactly the behaviour you intend.
query[util.promisify.custom] = (sql) => new Promise((resolve, reject) => { /* ... */ });
That symbol is how a library exposes something richer than the mechanical conversion — extra properties on the resolved value, for instance. Next: streams, where the callback model gives way to flow control, and the bug is no longer a pyramid but a memory leak.