Luka Vidaković

Cancelable Promise util


I found myself using this code and some derivatives time and time again so I decided to share. It’s not written by me, and I found it on Edd Mann’s blog. There are a few npm libraries that deal with promise cancellation in a similar manner, but I somehow prefer to have this few lines of source code somewhere in my util functions.

Here is the original function that wraps the native Promise and keeps a flag variable to allow us to cancel the .then chain anytime we want. Unfortunately the Promises themselves can’t really be canceled.

const cancelable = (promise) => {
  let hasCancelled = false

  return {
    promise: promise.then((v) => {
      if (hasCancelled) {
        throw { isCancelled: true }
      }

      return v
    }),
    cancel: () => (hasCancelled = true),
  }
}

When we call the cancelable function by giving it a promise we’ll get an object that has a:

Usage example:

// mocked fetch function to simulate waiting for a result 10 seconds
const fetchResult = () =>
  new Promise((resolve) => {
    setTimeout(() => resolve('response'), 10000)
  })

const { promise: result, cancel } = cancelable(fetchResult())

result.catch((error) => {
  if (error.isCancelled) console.log('Promise chain cancelled!')
})
result.then((res) => console.log(`Handler 1: ${res}`))
result
  .then((res) => console.log(`Handler 2: ${res}`))
  .then((res) => console.log(`Handler 3: ${res}`))

// at any point in time we can cancel all of the above success handlers by using cancel function
// catch handler can verify if cancellation is the reason of failure and do something based on it, in this case log out "Promise chain cancelled!"
cancel()

It’s important to note that by using this approach we can’t cancel out any handlers that were attached directly to an original promise object passed to our util function. This mechanism is only able to cancel out .then handlers appended to the returned promise. A bit weird but it’s not bad once you get used to it. You can still hold a reference to both the original and derived promise.

Another note is that error with isCancelled flag ends up in the catch handler only when the original Promise eventually resolves. All of this is essentially a way for us to say: once and if this Promise resolves, skip the success handlers because we are not interested in handling this data anymore.

I’m curious to hear from you of another similar approaches you might have and why they are better/worse 🍺