Cancelable Promise util
Tags: javascript
Published: 2019-11-29 20:53:15

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](https://eddmann.com/posts/implementing-a-cancelable-promise-in-javascript/). 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](https://github.com/tc39/proposal-cancelable-promises/issues/70).

```javascript
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:
- `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:

```javascript
// 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 🍺