Creating JavaScript Promises that can be resolved externally

Promises were a huge step forward for JavaScript. Finally, a mess of nested callbacks could be replaced with a fluent API.

Still, the built-in Promise has some limitations. As a replacement for callback functions, Promises are designed to wrap and represent a single function. When you create a Promise you hand control over to it by giving it two callbacks: resolve and reject. Then you wait until the function calls one of them:

new Promise((resolve, reject) => {
  // do something asynchronous here
  // call `resolve` or `reject` when finished
})
.then(result => {
  // this code can't execute until `resolve` or `reject` is called above
})

This means a standard Promise is simply a more organized way to handle callback results and errors.

That’s a problem because it limits abstraction. If a Promise is only a callback object, we limit it to that particular use case. But Promises can be a more flexible means of control flow. They represent pending action, and they indicate when it is finished.

A Promise can exist on its own without wrapping a callback function. To make this happen, we just need to pull its resolve and reject methods outside the scope of its callback:

const defer = () => {

  let resolve, reject

  const deferred = new Promise((res, rej) => {
    resolve = res
    reject = rej
  })

  deferred.resolve = resolve
  deferred.reject = reject

  return deferred
}

We now have a Promise with two additional methods: resolve and reject. We can use these together with then and catch to control flow any way we want.

Let’s break this down. defer is a factory function. It creates special Promise objects, so to use it we would assign the result to a variable:

const myPromise = defer()

We can attach callbacks to myPromise just like any other Promise:

myPromise
  .then(result => {
    console.log('It finished!', result)
  })
  .catch(err => {
    console.warn('Something went wrong', err)
  })

Finally, anywhere in our program that myPromise is in scope, we can resolve or reject it:

if (err) {
  myPromise.reject(err)
}
else {
  myPromise.resolve(value)
}

You might have noticed this looks a lot like a jQuery Deferred Object. This pattern gives us much of the same utility value without a library.

I’ll leave it to you to decide if it fits your current work.