JavaScript Promisification
Learn JavaScript promisification: wrap error-first callbacks in a Promise, build a generic promisify() helper, and handle multi-argument callbacks.
What Is Promisification?
Promisification is the act of wrapping a callback-based function so that it returns a Promise instead of taking a callback. You do it once, then you get to use .then(), .catch(), chaining, and async/await on a function that was never designed for them.
This page covers how to wrap a single error-first callback API, how to build a reusable generic promisify() helper, how to handle callbacks that return more than one result, and the cases where promisification does not work.
Why Promisify?
Older JavaScript APIs and most of the Node.js standard library report their results through a callback you pass in. That style nests quickly and scatters error handling:
getUser(id, (err, user) => {
if (err) return handleError(err);
getOrders(user, (err, orders) => {
if (err) return handleError(err);
getTotal(orders, (err, total) => {
if (err) return handleError(err);
console.log(total);
});
});
});If the same functions returned promises, the logic flattens into a single linear chain (or a few await lines) with one .catch() for the whole flow. Promisification is the bridge between these two worlds — see Callbacks and Beyond for the callback side of the story.
The Error-First Callback Convention
Before wrapping anything, you need to know the shape you are wrapping. Node.js callbacks follow the error-first (or "Node-style") convention: the callback is the last argument, and it is called as callback(error, result).
- On failure,
erroris anErrorobject andresultis undefined. - On success,
errorisnullandresultholds the value.
Promisification maps this directly: a non-null error becomes reject(error), and a successful result becomes resolve(result).
Wrapping a Single Callback API
Here is the core pattern. We wrap a callback-style function in a new Promise, calling reject for the error and resolve for the value. The example simulates an error-first API with setTimeout so it runs anywhere, including in the browser:
The wrapper accepts the same id argument, forwards it, and supplies its own callback that bridges into resolve/reject. The caller never sees a callback again.
Using the Wrapper with async/await
The real payoff of a promise-returning function is that it works with async/await, turning asynchronous code into something that reads top to bottom:
A Generic promisify() Helper
Writing a wrapper by hand for every function gets repetitive. A generic helper takes any error-first function and returns a promise-returning version of it. The trick is to collect all of the original arguments with a rest parameter, then append our own callback:
Because the helper uses ...args and forwards this, it works for functions with any number of leading arguments. In Node.js, the standard library ships exactly this as util.promisify, so you rarely need to write your own there:
const fs = require('fs');
const util = require('util');
const readFile = util.promisify(fs.readFile);
readFile('file.txt', 'utf8')
.then(data => console.log(data))
.catch(err => console.error(err));Handling Multi-Argument Callbacks
The simple helper assumes the callback delivers a single result: callback(err, result). Some APIs pass several values, such as callback(err, header, body). A plain resolve(result) would silently drop everything after the first value.
A promise can only resolve with one value, so collect the extra arguments into an array (or an object) and resolve with that:
Node's util.promisify supports the same idea through a custom symbol (util.promisify.custom), but for ad-hoc functions an array is the simplest approach.
Limitations and Gotchas
Promisification is mechanical, but it has real boundaries:
- It expects the error-first convention. If a function signals errors some other way — for example a boolean return, a thrown exception, or
(result, err)order — a generic helper will misread it. Wrap those by hand. - It only handles a single completion. Promises settle once. A function that invokes its callback repeatedly (events, streams,
setInterval, a progress callback) cannot be promisified — only the first call would resolve the promise; later calls are ignored. Use an event API or an async iterator for repeating values. - You cannot cancel a promise. If the underlying callback API supports cancellation (like clearing a timer), that ability is lost once it is hidden behind a promise.
- The wrapper changes the call signature. Callers must now use
.then/awaitinstead of passing a callback. Don't promisify a function that some code still calls in callback style without keeping both versions. - A throw inside the executor still rejects. Code that runs synchronously inside
new Promise((resolve, reject) => { ... })is caught and turned into a rejection — but an error thrown later inside an async callback is not caught automatically, which is exactly why you must callreject(err)explicitly.
Best Practices
- Promisify at the boundary. Convert I/O and timer APIs once, near where they enter your code, and keep the rest of your codebase promise-based.
- Prefer built-ins. In Node.js, reach for
util.promisify(or thefs/promises,dns/promises, etc. modules) before hand-rolling a wrapper. - Always handle rejection. Attach a
.catch()or wrapawaitintry/catch; an unhandled rejection can crash a Node process. - Keep names predictable. A common convention is to suffix the promise version with
Async(readFileAsync) so both styles can coexist.
Related Topics
- JavaScript Promise — the object you are creating when you promisify.
- Promises Chaining — sequence promisified calls cleanly.
- Async/Await — the syntax that makes promisified functions read like synchronous code.
- Callbacks and Beyond — the pattern you are converting from.
- Promise API — combine several promisified calls with
Promise.alland friends.