Lesson guide

Learning objectives

  • Explain the main purpose of Promisification in JavaScript.
  • Identify the syntax, APIs, or concepts introduced in this lesson.
  • Use the examples to predict how JavaScript will behave before you run similar code.
  • Connect this topic to nearby lessons in Promises, async/await.

Real-world context

This lesson matters when you need to recognize where Promisification fits into real JavaScript programs. “Promisification” is a long word for a simple transformation.

Key ideas

  • Promisification is part of the Promises, async/await chapter, so it builds on the surrounding concepts rather than standing alone.
  • Read each code example in two passes: first for the result, then for the rule that explains the result.
  • When a section compares similar features, focus on the condition that makes you choose one feature over another.

Key terms

  • Promisification
  • Promises, async/await
  • JavaScript

“Promisification” is a long word for a simple transformation. It’s the conversion of a function that accepts a callback into a function that returns a promise.

Such transformations are often required in real-life, as many functions and libraries are callback-based. But promises are more convenient, so it makes sense to promisify them.

For better understanding, let’s see an example.

For instance, we have loadScript(src, callback) from the chapter Introduction: callbacks.

javascript
function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(null, script);
  script.onerror = () => callback(new Error(`Script load error for ${src}`));

  document.head.append(script);
}

// usage:
// loadScript('path/script.js', (err, script) => {...})

The function loads a script with the given src, and then calls callback(err) in case of an error, or callback(null, script) in case of successful loading. That’s a widespread agreement for using callbacks, we saw it before.

Let’s promisify it.

We’ll make a new function loadScriptPromise(src), that does the same (loads the script), but returns a promise instead of using callbacks.

In other words, we pass it only src (no callback) and get a promise in return, that resolves with script when the load is successful, and rejects with the error otherwise.

Here it is:

javascript
let loadScriptPromise = function(src) {
  return new Promise((resolve, reject) => {
    loadScript(src, (err, script) => {
      if (err) reject(err);
      else resolve(script);
    });
  });
};

// usage:
// loadScriptPromise('path/script.js').then(...)

As we can see, the new function is a wrapper around the original loadScript function. It calls it providing its own callback that translates to promise resolve/reject.

Now loadScriptPromise fits well in promise-based code. If we like promises more than callbacks (and soon we’ll see more reasons for that), then we will use it instead.

In practice we may need to promisify more than one function, so it makes sense to use a helper.

We’ll call it promisify(f): it accepts a to-promisify function f and returns a wrapper function.

javascript
function promisify(f) {
  return function (...args) { // return a wrapper-function (*)
    return new Promise((resolve, reject) => {
      function callback(err, result) { // our custom callback for f (**)
        if (err) {
          reject(err);
        } else {
          resolve(result);
        }
      }

      args.push(callback); // append our custom callback to the end of f arguments

      f.call(this, ...args); // call the original function
    });
  };
}

// usage:
let loadScriptPromise = promisify(loadScript);
loadScriptPromise(...).then(...);

The code may look a bit complex, but it’s essentially the same that we wrote above, while promisifying loadScript function.

A call to promisify(f) returns a wrapper around f``(*). That wrapper returns a promise and forwards the call to the original f, tracking the result in the custom callback (**).

Here, promisify assumes that the original function expects a callback with exactly two arguments (err, result). That’s what we encounter most often. Then our custom callback is in exactly the right format, and promisify works great for such a case.

But what if the original f expects a callback with more arguments callback(err, res1, res2, ...)?

We can improve our helper. Let’s make a more advanced version of promisify.

  • When called as promisify(f) it should work similar to the version above.
  • When called as promisify(f, true), it should return the promise that resolves with the array of callback results. That’s exactly for callbacks with many arguments.
javascript
// promisify(f, true) to get array of results
function promisify(f, manyArgs = false) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      function callback(err, ...results) { // our custom callback for f
        if (err) {
          reject(err);
        } else {
          // resolve with all callback results if manyArgs is specified
          resolve(manyArgs ? results : results[0]);
        }
      }

      args.push(callback);

      f.call(this, ...args);
    });
  };
}

// usage:
f = promisify(f, true);
f(...).then(arrayOfResults => ..., err => ...);

As you can see it’s essentially the same as above, but resolve is called with only one or all arguments depending on whether manyArgs is truthy.

For more exotic callback formats, like those without err at all: callback(result), we can promisify such functions manually without using the helper.

There are also modules with a bit more flexible promisification functions, e.g. es6-promisify. In Node.js, there’s a built-in util.promisify function for that.

Please note:

Promisification is a great approach, especially when you use async/await (covered later in the chapter Async/await), but not a total replacement for callbacks.

Remember, a promise may have only one result, but a callback may technically be called many times.

So promisification is only meant for functions that call the callback once. Further calls will be ignored.

Common mistakes

  • Skipping the small examples and then missing the exact rule that Promisification depends on.
  • Copying code without changing one value at a time to see which part controls the result.
  • Treating similar-looking syntax or APIs as interchangeable before checking their edge cases.

Summary

  • Promisification gives you one more piece of the JavaScript mental model.
  • The examples in this lesson are the quickest way to check whether the rule is clear.
  • Revisit the key terms when you meet the same idea in later chapters.

Predict

Before running this promisification check, predict the four output lines. It covers the error-first callback convention, single-result versus many-result wrappers, preserving this, and rejection on callback errors.

javascript
Reveal explanation

The output is single:5, many:5,6, this:20,12, and error:failed. The simple wrapper resolves with only the first success result, so legacyMath gives 5 and drops 6. With manyArgs = true, all success results after the error argument are collected into an array. The wrapper calls f.call(this, ...), so tool.computePromise(2) still sees this.factor as 10. When the callback receives an error, the wrapper rejects and the final .catch handles it.

Try it

Remove f.call(this, ...args) and call f(...args) instead, then predict why tool.computePromise(2) stops using tool.factor.

Practice

  • Rewrite one example from this lesson without looking at the original, then run it and compare the result.
  • Change one input, operator, method call, or option in a code sample and predict what will happen before running it.
  • Explain Promisification in your own words as if you were reviewing it with another learner.

Keep learning

Continue with Microtasks when you are ready for the next lesson.