Resolve a promise from outside in JavaScript: practical use cases

Last updated on May 03, 2024
Resolve a promise from outside in JavaScript: practical use cases

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>
A simple app where users can create, save and publish posts.
Publish doesn't happen until after save.

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;
}
Post is saved before publish happens.

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:

The functionality works as before after the refactor.

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.

See also