How to Annul Promises in JavaScript
Tags: webdev, react, javascript, programming
Published: 2024-07-01 07:26:16

In JavaScript, you might already know [how to cancel a request](https://levelup.gitconnected.com/how-to-cancel-a-request-in-javascript-67f98bd1f0f5?utm_source=webdeveloper.beehiiv.com&utm_medium=newsletter&utm_campaign=how-to-annul-promises-in-javascript): 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?utm_source=webdeveloper.beehiiv.com&utm_medium=newsletter&utm_campaign=how-to-annul-promises-in-javascript). 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:

```typescript
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:

 ![](https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/63397059-8a9a-46dd-87aa-2e9cdcf35278/image.png?t=1719717804)

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.

### Building a Simple Sequential Request React Hook

We can further encapsulate a simple sequential request React hook:

```typescript
import { useCallback, useRef } from 'react';
​
type RequestFn<T> = (signal: AbortSignal) => Promise<T>;
​
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();
    },
  };
};
​
function useLatest<T>(value: T) {
  const ref = useRef(value);
  ref.current = value;
​
  return ref;
}
​
export function useSequentialRequest<T>(requestFn: RequestFn<T>) {
  const requestFnRef = useLatest(requestFn);
  const currentRequest = useRef<{ cancel: () => void } | null>(null);
​
  return useCallback(() => {
    if (currentRequest.current) {
      currentRequest.current.cancel();
    }
​
    const { run, cancel } = buildCancelableFetch(
      requestFnRef.current,
    );
    currentRequest.current = { cancel };
​
    const promise = run().then(res => {
      currentRequest.current = null;
      return res;
    });
​
    return promise;
  }, [requestFnRef]);
}
```

Then we can simply use it:

```typescript
import { useSequentialRequest } from './useSequentialRequest';
​
export function App() {
  const run = useSequentialRequest(async (signal: AbortSignal) => {
    const ret = await fetch('http://localhost:5000', { signal }).then(
      res => res.text(),
    );
    console.log(ret);
  });
​
  return (
    <button onClick={run}>Run</button>
  );
}
```

This way, when you click the button multiple times quickly, you will only get the latest request data, discarding the previous requests.

 ![](https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/3637d175-5aeb-4346-9a99-6c8dd4892b32/image.png?t=1719722300)

Do you have other use cases? Feel free to share with me!

*If you find this helpful, [**please consider subscribing**](https://webdeveloper.beehiiv.com/) to my newsletter for more insights on web development. Thank you for reading!*