A Simple Implementation of Abortable Promise Using AbortController with TypeScript

Thor Chen
3 min readMar 22, 2020

--

Github Repo: https://github.com/zzdjk6/simple-abortable-promise

Image source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

Short Story

Truth: a Promise can’t be aborted or canceled. It can only be fulfilled or rejected.

Trick: But we can reject a Promise early if we want :)

Long Story

Let’s face it, we all have the needs to abort or cancel the Promise sometimes. For example, in a single-page application, it may take a long time to load some data or do some processing, while the user wants to abort it and do something else.

Luckily, we do have a standard approach to abort the network request via Fetch API. That is, we can pass an AbortSignal to fetch, and abort it via AbortController:

// Initialize fetch call
const controller = new AbortController;
const promise = fetch(url, { signal: controller.signal })
// Abort
controller.abort()

Then the Promise returned by fetch will be rejected with an AbortError when the fetch call is aborted.

However, this approach is not easy to use because:

  1. We need to keep the reference of AbortController somewhere else to invoke .abort() when needed
  2. Every AbortController can only be used once and we have to create new ones after abort happens. That is, the only effective call to .abort() is the first time. After we called .abort() on an AbortController, the AbortSignal is used and remain aborted. If you assign an aborted AbortSignal to a new fetch calls and try to invoke .abort() on its controller, there will be no effect.
  3. It only works with fetch

How about finding a way to generalize this behavior to all Promises? Let’s have a try.

First of all, let’s imagine if AbortablePromise exists, how can we use it?

In my illusion, we can keep a reference of AbortablePromise and just call .abort() on that reference object to abort it like the code below:

// Init
const promise = new AbortablePromise((resolve, reject) => {...});
// Abort
promise.abort();

Then, to keep it simple and consistent with the behavior of Fetch API, the AbortablePromise should be rejected with anAbortError when it is aborted.

Based on the illusion, we can quickly come up with the implementation below:

AbortablePromise Version 1

The core part of this implementation to wrap the executor function and reject the Promise when receiving an abort event:

const wrappedExecutor: ExecutorFunction<T> = (resolve, reject) => {
abortSignal.addEventListener('abort', () => {
reject(new AbortError());
});
executor(resolve, reject);
};

Also, we attach a new abort method to the Promise object:

this.abort = () => {
abortController.abort();
};

Extended Story

Obviously our story can’t end here. To make AbortablePromise useful in real cases, we need to do a bit more.

Ideally, in an enhanced version of AbortablePromise, I think these features are necessary:

  1. When initialize an AbortablePromise, I want to access the abortSignal inside of the executor function so that I can do something more when abort happens. A typical example is to pass the abortSignal to fetch calls inside the executor, so that the fetch calls will also be aborted when the Promise is aborted.
  2. When abort an AbortablePromise, I want to give it a customized reason sometimes instead of using the default one.
  3. I want to quickly wrap a normal Promise as an AbortablePromise
  4. I want to make this solution works across different browsers. Apparently, AbortController and AbortSignal is not supported in IE and some old versions of browsers.

To achieve these feature points, we come up with the version 2:

AbortablePromise Version 2

Come and find more on my github :)

--

--

Thor Chen
Thor Chen

Written by Thor Chen

Passionate JavaScript/TypeScript Developer with a Full-stack Background