Creating Cancellable Promises

Abu Ashraf Masnun
5 min readFeb 13, 2019

--

In this post, I assume that you’re already familiar with Promises and their use cases. We know promises are objects which would give us the promised value sometimes in the future, when the operation is finished. Ideally, when an asynchronous operation starts, we receive a promise which would then resolve (complete successfully) or be rejected (fail) when the operation is complete.

Here’s a quick example of a function that waits a given period and then returns my name.

This works well but once we have called the delayedName function, there’s no way to cancel this operation. We want a way to cancel promises.

Cancelling Promise with a Function

There are reliable open source solutions for cancellable promises. Some promise implementations (ie. Bluebird) implement their own ways to cancel promises. In production or serious use cases, I would rather use them. But for learning purposes, let us have some fun ourselves. Let’s implement cancellable promises.

To allow cancelling an operation, we would have one main promise for our desired operation but we would also create one additional promise for signalling purpose. We would design this additional promise in a way that it can be controlled with a function. A function, when called, would reject this promise. This promise would be used internally and the caller of the function would not know about it. Meanwhile the main promise would represent the operation being run and we would return it to the caller.

When we run our main async operation, inside the executor function passed to the promise constructor, we would run both our async operation and add an error handler to the signalling promise. This now becomes a race in time. If the main operation succeeds and the signalling promise wasn’t rejected in the mean time, our main promise resolves with the value or error from the operation. If the signalling promise rejects before the main operation completes, we understand that the promise is being cancelled. We try to gracefully cancel / handle our main operation and reject with an error stating that the promise was cancelled.

Don’t worry if the strategy doesn’t make sense at first, let’s take a look at this code:

We wrote a function wait which waits a given time in milliseconds and then resolves with the string “ok”.

Our main operation here is the setTimeout operation which allows us to schedule a function after a given delay. We created a promise, signal which can be cancelled (read “rejected”) by calling the cancel function defined inside it. Then we write our main promise function which wraps around the setTimeout call. Here, we make the setTimeout call which is asynchronous and at the same time, we also wait for the signal promise to get rejected with an error.

This wait function returns the ret object which has a promise (the main promise) and a cancel function. If we call this function before the setTimeout operation has finished, it would make the signal promise rejected and thus trigger the event handler we added inside the main promise. That would in turn call rej(err) to reject our main promise. Afterwards, we do a clean up by clearing the timeout.

So essentially, calling ret.cancel() in time can cancel the main operation.

This is how we can use it:

Since we’re immediately calling the cancel() function, it should cancel.

Error: Promise was cancelled
at ret.cancel (/Users/masnun/Projects/test/test.js:5:14)
at Object.<anonymous> (/Users/masnun/Projects/test/test.js:27:1)
at Module._compile (internal/modules/cjs/loader.js:722:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:733:10)
at Module.load (internal/modules/cjs/loader.js:620:32)
at tryModuleLoad (internal/modules/cjs/loader.js:560:12)
at Function.Module._load (internal/modules/cjs/loader.js:552:3)
at Function.Module.runMain (internal/modules/cjs/loader.js:775:12)
at startup (internal/bootstrap/node.js:300:19)
at bootstrapNodeJSCore (internal/bootstrap/node.js:826:3)

But if we don’t cancel it, the promise resolves fine.

The code so far

Making it flexible

In our example, we have created a signalling promise ourselves and used it to mark cancellation. But we could have allowed the caller to provide us with a promise instead. This would allow the caller more flexibility, s/he just needs to pass us a promise. We don’t care what s/he does to reject this promise, if it’s rejected before our operation ends, we reject our main promise.

Let’s refactor our wait function to accept a signal promise:

The code is almost what we had before. We just removed the part where created the signal. It is now a parameter for the wait function to be passed by the caller.

To keep the example same, we would write a new function, which will, when invoked give us the signalling promise and the cancel function.

We can now connect them together:

We first create our signal promise which can be cancelled with cancel(). We then pass this promise to the newly refactored wait function.

Here’s the code in full:

In our case, we wanted our signal promise to be cancellable by a function. But we could have chosen anything else. May be a timeout too? You see where we’re heading?

Promises with a Timeout

We already have a mechanism where we can use one promise to signal the cancellation of another. In our case, we called a function to cancel. We can easily adapt the code to cancel after a timeout. That is the signal rejects after a given timeout. We pass this signal to our wait function and we’re done. If wait doesn’t finish before signal times out, we reject our main promise too.

Here’s the code:

Our signal times out at 500ms, so the main promise rejects (it can’t reach it’s 1000 ms wait time).

Here’s the code:

Remember, cancelling promises is not always easy. In our case, we were using a very simple timeout. But what if we’re making a http request? What if by the time we cancel, the request has already been made? What if we’re using a third party package from npm and the package doesn’t allow cancelling http requests?

There’s a lot to consider. When we want to cancel a promise, we also want to gracefully clean up after it.

--

--