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.
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.
exercise.js in the code-practice repo. The solution is solution.js in the same folder.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:
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.
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
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.
Keep reading