Turning a Callback API Into a Promise

How to wrap a callback-based utility in a promise, why you can't pass a promise directly into .then(), and why each step in a chain should do exactly one job.

September 6, 20263 min read14 / 17

Here's the actual idea this post teaches, in one sentence: wrapping an existing callback-based function in new Promise(...) is called lifting, and it's the only extra step needed to turn any old utility into something you can chain.

This is the same coordination problem from a few posts back, inventory, shipping, payments, printed in order regardless of which one actually finishes first. Now that chaining is in the toolbox, here's what solving it with promises actually looks like.

Not Every Utility Speaks Promises

Plenty of real code you'll call, especially older code, only knows how to take a callback. It has no idea what a promise is.

"Lifting" is the name for wrapping a function like that in a promise, so the rest of your code never has to know it wasn't promise-aware to begin with.

ℹ️ Try it yourself first. The unsolved starter is exercise.js in the code-practice repo. The solution is solution.js in the same folder.
JavaScript
function fakeFetchStat(name, cb) { const delay = 300 + Math.random() * 700; setTimeout(() => cb(`${name} data ready`), delay); } function getStat(name) { return new Promise((resolve) => fakeFetchStat(name, resolve)); }

getStat doesn't do any new work. It just calls fakeFetchStat the same way it always worked, and hands resolve in as the callback. fakeFetchStat never has to change. Everything above it can now use promises like normal.

Three at Once, Same as Always

Creating the promises is the easy part, same as it was with thunks:

JavaScript
const inventoryPromise = getStat('inventory'); const shippingPromise = getStat('shipping'); const paymentsPromise = getStat('payments');

All three requests are running the instant these three lines finish. The real work is chaining their responses together in the right order.

The Gotcha: You Can't Just Hand a Promise to .then()

You already know .then() waits for whatever promise you return from inside it. The trap is skipping that step: writing p1.then(p2), handing a promise straight in instead of a function that returns one.

.then() only accepts a function, so p1.then(p2) fails outright. Wrap the promise in a function instead.

JavaScript
function output(text) { console.log(text); } function chainToShipping() { return shippingPromise; } function chainToPayments() { return paymentsPromise; } function complete() { output('All stats loaded'); } inventoryPromise .then(output) .then(chainToShipping) .then(output) .then(chainToPayments) .then(output) .then(complete);

Give Each Step Exactly One Job

Notice output only prints. chainToShipping only returns the next promise. Neither one does both.

It's tempting to combine them, print the text and return the next promise in the same function. Resist that. Keeping "respond to this step" and "move to the next step" as separate .then() calls means each one only has to answer one question, which is the entire reason a chain reads as a clean vertical list instead of a puzzle.

This also answers a question worth asking upfront: should these be named functions, like output and chainToShipping, or quick inline ones written straight into the chain? Named functions win here. A chain full of inline function expressions loses the one real readability benefit chaining gives you, being able to scan straight down the list and know what each step does at a glance.

Run It and Watch the Order Hold

JavaScript
inventoryPromise .then(output) .then(chainToShipping) .then(output) .then(chainToPayments) .then(output) .then(complete);

Run this a few times. The random delay means the three requests finish in a different order every time.

The printed order never changes: inventory, shipping, payments, every single run. No shared object. No loop checking what's arrived. Just six small steps, each one only responsible for the one thing its name says it does.