Skip to main content

What is promisification?

Promisification converts a callback-based function into one that returns a Promise, so you can use .then(), .catch(), or async/await instead of nested callbacks.

Theory

TL;DR

  • Callbacks are like passing a note to a friend: "call me when done." Promisification wraps that into a request with a clear result, either success or failure.
  • Main difference: replaces (err, result) => {} nesting with .then(result => {}).catch(err => {}).
  • Use it for legacy Node.js APIs (fs.readFile, crypto) and old third-party libraries. Skip it when the API already returns a Promise.
  • util.promisify handles the standard Node.js (err, value) callback pattern automatically.

Quick example

javascript
const fs = require('fs'); const util = require('util'); // Original callback style fs.readFile('data.txt', 'utf8', (err, data) => { if (err) return console.error(err); console.log(data); // Output: file contents }); // Promisified - same result, no nesting const readFile = util.promisify(fs.readFile); const data = await readFile('data.txt', 'utf8'); console.log(data); // Output: file contents

Both do the same thing. The promisified version lets you await it and handle errors with a single try/catch.

Key difference

The Node.js callback pattern expects (err, result) as the last argument of every async call. Promisification wraps that function in a new one that creates a Promise internally, passes a generated callback to the original, and resolves or rejects based on what comes back. You write the wrapper once and use it everywhere.

When to use

  • Node.js built-in APIs (fs, crypto, dns) that predate Promises: promisify them and switch to async/await.
  • Third-party libraries stuck on callback style: wrap once, chain everywhere.
  • Legacy code you cannot rewrite: promisify the interface, leave the internals untouched.
  • API already returns a Promise: skip promisification entirely.

How it works internally

util.promisify (added in Node.js v8.0.0) checks if the target function has a util.promisify.custom symbol defined. If not, it assumes the last argument is a standard (err, value) callback. It returns a wrapper that creates a new Promise, calls the original with a generated callback, and either calls resolve(value) or reject(err).

Common mistakes

Mistake: promisifying functions that do not follow the Node.js error-first style.

javascript
const sleep = util.promisify(setTimeout); // TypeError: callback is not a function

setTimeout takes a plain callback, not (err, value). Use new Promise(resolve => setTimeout(resolve, 1000)) directly.

Mistake: losing this when promisifying class methods.

javascript
class DB { read(id, cb) { cb(null, 'data'); } } const db = new DB(); const read = util.promisify(db.read); // this = undefined inside read

Fix: util.promisify(db.read.bind(db)). Bind the instance before wrapping. This one causes silent failures in production more often than the callback signature mismatch.

Mistake: expecting a single resolved value when the original passes multiple.

javascript
const lookup = util.promisify(dns.lookup); lookup('example.com').then(address => console.log(address)); // wrong - address is [address, family]

dns.lookup calls its callback with (err, address, family). util.promisify resolves to an array [address, family]. Destructure: .then(([address, family]) => ...).

Real-world usage

  • Express: util.promisify(fs.access) before serving static files.
  • MongoDB callback driver (pre-async): util.promisify(collection.findOne) in older Lambda handlers.
  • AWS SDK v2: util.promisify(s3.getObject) for S3 operations.
  • Legacy internal APIs: wrap once at the module boundary, leave callback internals unchanged.

Follow-up questions

Q: What is the difference between util.promisify and a manual new Promise wrapper?
A: util.promisify auto-handles the (err, value) pattern and respects util.promisify.custom. A manual wrapper gives full control but you write the callback logic yourself. For standard Node.js APIs, util.promisify is shorter and less likely to have bugs.

Q: How does util.promisify handle functions that pass multiple values to the callback?
A: It resolves to an array of all arguments after err. For dns.lookup that is [address, family]. Destructure to get each value separately.

Q: Can you promisify a synchronous function?
A: Yes, but there is no point. The Promise resolves immediately and you added overhead for nothing.

Q: Why avoid util.promisify for functions that work with streams?
A: Streams need backpressure handling. Promisify resolves once and exits, ignoring the ongoing data flow. That can cause memory leaks. Use stream.pipeline or async iterators instead.

Examples

Callback hell vs promisification

Reading two files in sequence with callbacks creates a pyramid. Promisification keeps it flat.

javascript
const fs = require('fs'); const util = require('util'); // Callback version - nesting grows with each sequential call fs.readFile('data.txt', 'utf8', (err, data1) => { if (err) return console.error(err); fs.readFile('data2.txt', 'utf8', (err, data2) => { if (err) return console.error(err); console.log(data1 + data2); // Output: combined file contents }); }); // Promisified - same result, reads top to bottom const readFile = util.promisify(fs.readFile); try { const data1 = await readFile('data.txt', 'utf8'); const data2 = await readFile('data2.txt', 'utf8'); console.log(data1 + data2); // Output: combined file contents } catch (err) { console.error(err); }

Same logic, same output. Every sequential step is one line, not one nesting level.

Express route with promisified file reads

A practical pattern: reading user config from disk before sending an API response.

javascript
const fs = require('fs'); const util = require('util'); const readFile = util.promisify(fs.readFile); app.get('/user/:id', async (req, res) => { try { const userData = await readFile(`users/${req.params.id}.json`, 'utf8'); const settings = await readFile('app-settings.json', 'utf8'); res.json({ user: JSON.parse(userData), // Output: user object settings: JSON.parse(settings) // Output: settings object }); } catch (err) { res.status(500).json({ error: err.message }); // Output: { error: 'ENOENT...' } } });

One try/catch covers both reads. Without promisification this is two nested callbacks with separate error branches each.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?