A Simple Implementation of Abortable Promise Using AbortController with TypeScript
Github Repo: https://github.com/zzdjk6/simple-abortable-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:
- We need to keep the reference of
AbortControllersomewhere else to invoke.abort()when needed - Every
AbortControllercan 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 anAbortController, theAbortSignalis used and remainaborted. If you assign an abortedAbortSignalto a new fetch calls and try to invoke.abort()on its controller, there will be no effect. - 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 1The 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:
- When initialize an
AbortablePromise, I want to access theabortSignalinside of theexecutorfunction so that I can do something more when abort happens. A typical example is to pass theabortSignaltofetchcalls inside theexecutor, so that thefetchcalls will also be aborted when thePromiseis aborted. - When abort an
AbortablePromise, I want to give it a customized reason sometimes instead of using the default one. - I want to quickly wrap a normal
Promiseas anAbortablePromise - I want to make this solution works across different browsers. Apparently,
AbortControllerandAbortSignalis not supported in IE and some old versions of browsers.
To achieve these feature points, we come up with the version 2:
AbortablePromise Version 2Come and find more on my github :)
