Trust is earned, not given

A different perspective

2018-05-14 · Projects

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:

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.