It’s one of those “cool” things you can do in JavaScript that are actually immensely powerful when put to good use.
let promiseResolve;
let promiseReject;
const promise = new Promise((resolve, reject) => {
promiseResolve = resolve;
promiseReject = reject;
});
promiseResolve();
Powerful practical use cases
Action B waiting for action A
A is ongoing but the user wants to do B but A needs to happen first.
Example: Social app where users can create, save, and publish posts. Like Medium.
<p>
Save status:
<b><span id="save-status">Not saved</span></b>
</p>
<p>
Publish status:
<b><span id="publish-status">Not published</span></b>
</p>
<button id="save">Save</button>
<button id="publish">Publish</button>
What if the user wants to publish the post when it’s saving?
Solution: Ensure the post is saved before publishing happens.
saveButton.onclick = () => {
save();
};
publishButton.onclick = async () => {
await publish();
};
let saveResolve;
let hasSaved = false;
async function save() {
hasSaved = false;
saveStatus.textContent = 'Saving...';
// ✅ Resolve promise from outside
await makeSaveRequest();
saveResolve();
hasSaved = true;
saveStatus.textContent = 'Saved';
}
async function waitForSave() {
if (!hasSaved) {
await new Promise((resolve) => {
saveResolve = resolve;
});
}
}
async function publish() {
publishStatus.textContent = 'Waiting for save...';
await waitForSave();
publishStatus.textContent = 'Published';
return;
}
It gets even better when you abstract this logic into a kind of Deferred
class:
class Deferred {
constructor() {
this.promise = new Promise((resolve, reject) => {
this.reject = reject;
this.resolve = resolve;
});
}
}
const deferred = new Deferred();
// Resolve from outside
deferred.resolve();
Refactoring✅:
// ...
const deferredSave = new Deferred();
let hasSaved = false;
async function save() {
hasSaved = false;
saveStatus.textContent = 'Saving...';
// ✅ Resolve promise from outside
await makeSaveRequest();
saveResolve();
hasSaved = true;
saveStatus.textContent = 'Saved';
}
async function waitForSave() {
if (!hasSaved) await deferredSave.promise;
}
async function publish() {
// ...
}
And it works exactly like before:
Deferred
is much cleaner, which is why we’ve got tons of NPM libraries like it: ts-deferred
, deferred
, promise-deferred
…
Promisifying an event stream
It’s a great setup I’ve used multiple times.
Doing an async task that’s actually waiting for an event stream to fire, internally:
// data-fetcher.js
const deferred = new Deferred();
let dataDeferred;
function startListening() {
dataDeferred = new Deferred();
eventStream.on('data', (data) => {
dataDeferred.resolve(data);
});
}
async function getData() {
return await dataDeferred.promise;
}
class Deferred {
constructor() {
this.promise = new Promise((resolve, reject) => {
this.reject = reject;
this.resolve = resolve;
});
}
}
// client.js
const { startListening, getData } = require('./data-fetcher.js');
startListening();
const data = await getData();
Final thoughts
Resolving promises externally unlocks powerful patterns.
From user actions to event streams, it keeps your code clean and flexible. Consider libraries like ts-deferred
for even better handling.
Every Crazy Thing JavaScript Does
A captivating guide to the subtle caveats and lesser-known parts of JavaScript.