In JavaScript, you might already know [how to cancel a request](https://levelup.gitconnected.com/how-to-cancel-a-request-in-javascript-67f98bd1f0f5): you can use `xhr.abort()` for XHR and `signal` for fetch. But how do you cancel a regular Promise?

Currently, JavaScript's Promise does not natively provide an API to cancel a regular Promise. **So, what we’ll discuss next is how to discard/ignore the result of a Promise.**

## Method 1: Using the New Promise.withResolvers()

A new API that can now be used is [Promise.withResolvers()](https://levelup.gitconnected.com/new-async-api-promise-withresolvers-simplifies-your-code-1355784fb435). It returns an object containing a new Promise object and two functions to resolve or reject it.

Here’s how the code looks:

```javascript
let resolve, reject;
const promise = new Promise((res, rej) => {
  resolve = res;
  reject = rej;
});
```

Now we can do this:

```javascript
const { promise, resolve, reject } = Promise.withResolvers();
```

So we can utilize this to expose a `cancel` method:

```typescript
const buildCancelableTask = <T>(asyncFn: () => Promise<T>) => {
  let rejected = false;
  const { promise, resolve, reject } = Promise.withResolvers<T>();

  return {
    run: () => {
      if (!rejected) {
        asyncFn().then(resolve, reject);
      }

      return promise;
    },

    cancel: () => {
      rejected = true;
      reject(new Error('CanceledError'));
    },
  };
};
```

Then we can use it with the following test code:

```typescript
const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms));

const ret = buildCancelableTask(async () => {
  await sleep(1000);
  return 'Hello';
});

(async () => {
  try {
    const val = await ret.run();
    console.log('val: ', val);
  } catch (err) {
    console.log('err: ', err);
  }
})();

setTimeout(() => {
  ret.cancel();
}, 500);
```

Here, we preset the task to take at least 1000ms, but we cancel it within the next 500ms, so you will see:

![JavaScript Promise cancellation example using Promise.withResolvers showing early rejection](https://img.upweb.dev/content/6fce3f92-623x169.jpg)

Note that this is not true cancellation but an early rejection. The original `asyncFn()` will continue to execute until it resolves or rejects, but it doesn’t matter because the promise created with `Promise.withResolvers<T>()` has already been rejected.

## Method 2: Using AbortController

Just like we cancel fetch requests, we can implement a listener to achieve early rejection. It looks like this:

```typescript
const buildCancelableTask = <T>(asyncFn: () => Promise<T>) => {
  const abortController = new AbortController();

  return {
    run: () =>
      new Promise<T>((resolve, reject) => {
        const cancelTask = () => reject(new Error('CanceledError'));

        if (abortController.signal.aborted) {
          cancelTask();
          return;
        }

        asyncFn().then(resolve, reject);

        abortController.signal.addEventListener('abort', cancelTask);
      }),

    cancel: () => {
      abortController.abort();
    },
  };
};
```

It has the same effect as mentioned above but uses AbortController. You can use other listeners here, but AbortController provides the additional benefit that if you call `cancel` multiple times, it won’t trigger the `'abort'` event more than once.

Based on this code, we can go further to build a cancelable fetch. This can be useful in scenarios like sequential requests, where you might want to discard previous request results and use the latest request results.

```typescript
const buildCancelableFetch = <T>(
  requestFn: (signal: AbortSignal) => Promise<T>,
) => {
  const abortController = new AbortController();

  return {
    run: () =>
      new Promise<T>((resolve, reject) => {
        if (abortController.signal.aborted) {
          reject(new Error('CanceledError'));
          return;
        }

        requestFn(abortController.signal).then(resolve, reject);
      }),

    cancel: () => {
      abortController.abort();
    },
  };
};

const ret = buildCancelableFetch(async (signal) => {
  return fetch('http://localhost:5000', { signal }).then((res) => res.text());
});

(async () => {
  try {
    const val = await ret.run();
    console.log('val: ', val);
  } catch (err) {
    console.log('err: ', err);
  }
})();

setTimeout(() => {
  ret.cancel();
}, 500);
```

Please note that this does not affect the server-side processing logic; it merely causes the browser to discard/cancel the request. In other words, if you send a POST request to update user information, it may still take effect. Therefore, this is more commonly used in scenarios where a GET request is made to fetch new data.

We can take it a step further by building a React hook that handles sequential requests, making it easier to reuse this logic, improve app performance, and enhance user experience. For more details, check out this article: "[How to Build a React Hook That Handles Sequential Requests](https://upweb.dev/posts/build-sequential-request-react-hook)."

_If you found this helpful, consider [subscribing to my newsletter](https://upweb.dev) for weekly web development insights and updates. Thanks for reading!_
