W3docs

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, error is an Error object and result is undefined.
  • On success, error is null and result holds 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:


javascript— editable

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:


javascript— editable

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:


javascript— editable

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:


javascript— editable

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/await instead 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 call reject(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 the fs/promises, dns/promises, etc. modules) before hand-rolling a wrapper.
  • Always handle rejection. Attach a .catch() or wrap await in try/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.

Practice

Practice
What is the main function of promisification in JavaScript?
What is the main function of promisification in JavaScript?
Was this page helpful?