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
AbortController
somewhere else to invoke.abort()
when needed - 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 anAbortController
, theAbortSignal
is used and remainaborted
. If you assign an abortedAbortSignal
to 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:
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:
- When initialize an
AbortablePromise
, I want to access theabortSignal
inside of theexecutor
function so that I can do something more when abort happens. A typical example is to pass theabortSignal
tofetch
calls inside theexecutor
, so that thefetch
calls will also be aborted when thePromise
is 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
Promise
as anAbortablePromise
- I want to make this solution works across different browsers. Apparently,
AbortController
andAbortSignal
is not supported in IE and some old versions of browsers.
To achieve these feature points, we come up with the version 2:
Come and find more on my github :)