DEV Community

Luka Vidaković
Luka Vidaković

Posted on

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
  }
};
Enter fullscreen mode Exit fullscreen mode

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

  • promise property, original promise extended with a single .then handler that is able to cancel out all the following .then handlers appended to it later on. Cancellation is based on a local variable hasCancelled. If the flag turns to true before the promise resolves it throws with additional information and bypasses all the latter .then handlers. We should use this property instead of our original promise and append any required .then handlers to it.
  • cancel method that changes the local hasCancelled flag to true

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()
Enter fullscreen mode Exit fullscreen mode

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 🍺

Top comments (0)