cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Promisification & promise-based APIs

Wrap callback-based JavaScript APIs in promises, understand Node’s util.promisify, and design async functions that are predictable to use.

By the end, you can
  • 01
    Promisify callback functionsTurn an error-first callback API into a promise-returning function.
  • 02
    Spot wrapper limitsExplain extra callback results, lost this, repeated callbacks, and non-error-first APIs.
  • 03
    Design clean async APIsReturn promises consistently, reject with Error objects, and document resolved values.

Why promisify?

A lot of JavaScript was written before promises became the everyday way to represent future results. Those older APIs often say, “give me a callback and I’ll call it later.” Promisification means wrapping that callback API in a new function that returns a promise instead.

You already met callbacks in the Callbacks lesson and promises in the Promises lesson. This lesson connects them: old socket, new plug. The old work stays the same; the calling style becomes promise-friendly.

Real-life analogyA travel adapter for old sockets

If your charger does not fit the hotel socket, you do not rebuild the hotel. You add an adapter. A promisifier is that adapter: your newer promise code plugs into an older callback-shaped function.

In real life: A wall socket in another country
In JavaScript: An old callback API
In real life: Your laptop charger
In JavaScript: Your promise-based code
In real life: The travel adapter
In JavaScript: A promisify wrapper
In real life: Electricity still comes from the same wall
In JavaScript: The original async work still does the real job

Where the analogy stops: An adapter does not make electricity faster or safer. Likewise, promisify does not make the underlying work faster, cancellable, or repeated-event friendly by itself.

One-sentence definition

Promisification wraps a function that reports success or failure through callbacks and exposes a new function that reports success or failure by resolving or rejecting a promise.

Writing a promisify helper

STEP THROUGH

The common Node-style callback convention is called error-first: the callback receives (err, result). If err is an Error, the operation failed. If err is null, the next argument is the success value.

A manual wrapperPop out in the code editor (opens in a new tab)JavaScript
function readConfigPromise(name) {  return new Promise((resolve, reject) => {    readConfig(name, (err, config) => {      if (err) {        reject(err);        return;      }      resolve(config);    });  });}

A generic helper does the same pattern for many functions: return a new function, create a promise, call the original function with all normal arguments, and add one final callback that resolves or rejects.

Promisify an error-first callback
Step 0 of 5Ready
Your turn: follow the blue line

Change the config name, predict whether the wrapper resolves or rejects, then step through the adapter.

Running in
  1. script
Next: line 12
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
function readConfig(name, callback) {  setTimeout(() => {    if (name === "missing") {      callback(new Error("No config named " + name));      return;    }     callback(null, { name, theme: "dark" });  }, 10);}   return function (...args) {    return new Promise((resolve, reject) => {      fn(...args, (err, value) => {        if (err) reject(err);        else resolve(value);      });    });  };} const readConfigPromise = promisify(readConfig);readConfigPromise("app")  .then(config => console.log(config.theme))  .catch(error => console.log(error.message));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Choose the file name passed to line 13

Changing this starts a fresh replay; the original callback API stays the same.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Where generic wrappers break

INTERACTIVE

A generic promisifier assumes two things: the callback is the last argument, and the callback is error-first. That covers many Node APIs, but not every API you meet in browsers, UI libraries, or old codebases.

Promisify it yourself
Generic wrapperPop out in the code editor (opens in a new tab)JavaScript
const fsLike = {  prefix: "user:",  readFile(name, cb) {    cb(null, this.prefix + name.toUpperCase());  }}; function genericPromisify(fn) {  return (...args) => new Promise((resolve, reject) => {    fn(...args, (err, value) => err ? reject(err) : resolve(value));  });}
error-first readFilegeneric

generic works: user:ADA

  • Assumptioncallback last, (err, value)
  • Lessongeneric wrappers have limits
Try it yourself

generic works: user:ADA

A generic promisifier is useful, but only when the old API really follows the error-first, callback-last shape. Toggle the custom wrapper to see the repair.
Generic promisify assumptions and what to do when they fail
CaseWhat happensBetter wrapper
Error-first callbackWorks: (err, value) maps cleanly to reject or resolve.A generic promisify(fn) is fine.
Non-error-first callbacksA loader with onLoad and onError has no single (err, value) callback.Write a custom promise around both callbacks.
Two success valuesDefault wrappers keep one fulfillment value, so width, height loses one.Resolve an object like { width, height }.
Method uses thisPassing obj.method detaches it from obj.Use obj.method.bind(obj) or close over obj.
Callback fires more than onceThe promise settles once; later callback calls are ignored.Use events or async iterators for streams and clicks.

Node’s util.promisify

NODE

Node has shipped util.promisify since Node 8. It follows the same error-first, callback-last convention and returns a new function. In modern Node, many modules also expose promise-native APIs, so you often do not need an adapter at all.

Node promise APIsPop out in the code editor (opens in a new tab)JavaScript
import { promisify } from "node:util";import { readFile } from "node:fs";import { readFile as readFilePromise } from "node:fs/promises";import { setTimeout as sleep } from "node:timers/promises"; const readFileAsync = promisify(readFile);const text = await readFileAsync("package.json", "utf8");const sameText = await readFilePromise("package.json", "utf8");await sleep(100);

Some callback APIs have unusual success values. A function can define util.promisify.custom, a symbol, to tell util.promisify what promise wrapper to use instead of the default one.

Custom promisified behaviorPop out in the code editor (opens in a new tab)JavaScript
import { promisify } from "node:util"; function size(path, callback) {  callback(null, 640, 480);} size[promisify.custom] = path =>  Promise.resolve({ path, width: 640, height: 480 }); const sizeAsync = promisify(size);console.log(await sizeAsync("hero.png"));
Important limit

By default, util.promisify resolves with the first success value after err. Extra results are dropped unless the function defines a custom promisified version.

Wrapping timers, images, and geolocation

INTERACTIVE

Browser APIs do not all use Node’s error-first style. You still use the same promise constructor idea, but the wrapper must match the original API: setTimeout has one success callback, img.onload and img.onerror are separate events, and geolocation has success and error callbacks plus user permission.

Real browser wrappers
Promise wrappersPop out in the code editor (opens in a new tab)JavaScript
const sleep = ms => new Promise(resolve => {  setTimeout(resolve, ms);}); function loadImage(src) {  return new Promise((resolve, reject) => {    const img = new Image();    img.onload = () => resolve(img);    img.onerror = () => reject(new Error("Image failed to load"));    img.src = src;  });} function getPosition(options) {  return new Promise((resolve, reject) => {    if (!navigator.geolocation) {      reject(new Error("Geolocation is not available"));      return;    }    navigator.geolocation.getCurrentPosition(resolve, reject, options);  });}
Live resultidle

Choose a wrapper and run it. Nothing asks for permission on load.

Geolocation errors use codes 1 permission denied, 2 position unavailable, and 3 timeout.

Try it yourself

Choose a wrapper and run it. Nothing asks for permission on load.

The geolocation button is opt-in. This demo rounds any coordinates locally and never sends them anywhere.

Geolocation requires a secure context such as HTTPS or localhost and the user’s permission. Its error codes are 1 PERMISSION_DENIED, 2 POSITION_UNAVAILABLE, and 3 TIMEOUT. Never prompt on page load; only ask after a clear user action.

Designing promise-based APIs

SORT
Real-life analogyA good vending machine

A vending machine would be annoying if candy fell out instantly sometimes, appeared in a side drawer other times, and occasionally shouted an error with no label. Good promise APIs use one slot: a promise every time.

In real life: Every snack comes from the same slot
In JavaScript: Every async function returns a promise
In real life: A clear out-of-stock message
In JavaScript: Reject with an Error object
In real life: A cancel button before the vend finishes
In JavaScript: Accept an AbortSignal option for long work

Where the analogy stops: A vending machine hides many details. API design should document the details: what it resolves with, when it rejects, and whether cancellation is supported.

Good async API design?
  • Always returns a promise, even when cached
  • Calls its callback synchronously sometimes
  • Rejects with Error objects or Error subclasses
  • Rejects with plain strings like "nope"
  • Accepts an AbortSignal option for long work
  • Resolves { width, height } instead of two positional values
  • Sometimes accepts a callback and sometimes returns a promise
  • Throws synchronously from a promise-returning function for bad input
Try it yourself
0 of 8 correct

Sort each design choice into the side you would want in a promise-based API.

Choose a category for every card. You can change an answer at any time; Reset clears them all.
A promise-shaped APIPop out in the code editor (opens in a new tab)JavaScript
async function getUserProfile(id, { signal } = {}) {  if (typeof id !== "string") {    throw new TypeError("id must be a string");  }   const response = await fetch(`/api/users/${id}`, { signal });  if (!response.ok) {    throw new Error(`Profile request failed: ${response.status}`);  }   return response.json();}

In an async function, a bad argument becomes a rejected promise, so callers can handle validation, network errors, and server errors in one try/catch or .catch. Document what the promise resolves to, and avoid APIs that mix callback and promise styles in the same function.

Where you’ll use this

Promisification shows up when you gradually modernize older code. You might wrap a callback-only library so the rest of your app can use promise chains, or write browser wrappers so one feature can use the same error handling style as the rest of your promise code.

Real-life analogyWe’ll call you vs take this token

Callback style says, “give us your phone number; we’ll call you.” Promise style says, “take this token; it represents your order.” Promisification gives you the token while the old code keeps working.

In real life: A shop takes your phone number
In JavaScript: You pass a callback and the API controls when it calls
In real life: A shop gives you an order token
In JavaScript: The function returns a promise you can hold, pass around, and await
In real life: The shop does the same work
In JavaScript: The wrapper changes control flow, not the work itself

Where the analogy stops: A promise is not a physical token. Only the code that created it can settle it.

Common misconceptions

The timing trap called ZalgoPop out in the code editor (opens in a new tab)JavaScript
// Avoid: sometimes callback now, sometimes later.function getCached(key, callback) {  if (cache.has(key)) callback(null, cache.get(key));  else setTimeout(() => callback(null, load(key)), 0);} // Prefer: one promise-shaped result every time.async function getCached(key) {  if (cache.has(key)) return cache.get(key);  return load(key);}
  • “Promisify makes work faster.” No. It changes the interface, not the timer, disk, network, or permission prompt underneath.
  • “Every callback should become one promise.” No. Repeating events such as clicks, data streams, and progress updates do not fit one settlement.
  • “Resolve can happen many times.” A promise settles once. Later resolve or reject calls are ignored.
  • “Generic wrappers understand every callback shape.” They assume callback-last and error-first unless you customize them.
  • “Rejecting a string is fine.” Prefer Error objects so catch blocks get a message, name, and debugging context.
Callback API vs promise API
QuestionCallback stylePromise style
Who holds the result?The API calls your function later.You hold a promise immediately.
Failure pathUsually first callback argument: err.Rejected promise, caught with .catch or try/catch around await.
Multiple resultsCallback can pass several positional values.Prefer one object with named properties.
Repeated eventsCallbacks can fire many times.One promise settles once; use events or async iterators later.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict a promise that resolves twice

Read the program. What does it print after the microtask runs?

Starter codePop out in the code editor (opens in a new tab)JavaScript
function oldApi(callback) {
  callback(null, "first");
  callback(null, "second");
}

const once = (...args) => new Promise((resolve, reject) => {
  oldApi((err, value) => err ? reject(err) : resolve(value));
});

once().then(value => console.log(value));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeWrite a sleep(ms) wrapper

    Create a function that returns a promise fulfilled by a timer.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    function sleep(ms) {
      // return a promise here
    }
      Exercise 3 · PracticeFind the lost this bug

      Which method fixes a function that lost its object as this?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const read = promisifyOne(reader.read);
      read("notes.txt").then(console.log);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeRemember util.promisify’s default result

        A callback calls cb(null, "first", "second"). What success value does the default promisified function keep?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeHandle geolocation denial

          When a user says no to the browser’s location prompt, which standard geolocation error code should your rejection expose?

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            7 QUESTIONS
            Promisification quiz · 7 questionsScore: first tries count
            1. Question 1 of 7What convention does a generic Node-style promisify helper assume?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does this print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              let settle;
              const p = new Promise(resolve => { settle = resolve; });
              settle("first");
              settle("second");
              p.then(value => console.log(value));

              Choose an answer to see the explanation.

            3. Question 3 of 7Why can passing obj.method to promisify break?

              Choose an answer to see the explanation.

            4. Question 4 of 7By default, what happens to extra success values in util.promisify?

              Choose an answer to see the explanation.

            5. Question 5 of 7Which browser wrapper should only run after a user clicks a button?

              Choose an answer to see the explanation.

            6. Question 6 of 7Which design avoids releasing Zalgo?

              Choose an answer to see the explanation.

            7. Question 7 of 7What should a promise reject with?

              Choose an answer to see the explanation.

            Key takeaways

            • Promisification adapts callback APIs; it does not change the underlying work.
            • Generic wrappers assume callback-last and error-first: (err, value).
            • util.promisify keeps the first success value unless a custom symbol overrides it.
            • Promises settle once, so repeating events need events or async iterators instead.
            • Good async APIs always return promises, reject with Error objects, and document their resolved value.

            Promisification is the adapter that turns “I’ll call your callback” into “here is a promise for the result.”

            Up next: async & await.

            CompleteFrontend Clear concepts. Working examples.