Based off your code, there are a few corrections to make:

Don't return new Promise() inside an async function

You use new Promise if you're taking something event-based but naturally asynchronous, and wrap it into a Promise. Examples:

  • setTimeout
  • Web Worker messages
  • FileReader events

But in an async function, your return value will already be converted to a promise. Rejections will automatically be converted to exceptions you can catch with try/catch. Example:

async function MyAsyncFunction(): Promise<number> {
  try {
    const value1 = await functionThatReturnsPromise(); // unwraps promise 
    const value2 = await anotherPromiseReturner();     // unwraps promise
    if (problem)
      throw new Error('I throw, caller gets a promise that is eventually rejected')
    return value1 + value2; // I return a value, caller gets a promise that is eventually resolved
  } catch(e) {
    // rejected promise and other errors caught here
    console.error(e);
    throw e; // rethrow to caller
  }
}

The caller will get a promise right away, but it won't be resolved until the code hits the return statement or a throw.

What if you have work that needs to be wrapped with a Promise constructor, and you want to do it from an async function? Put the Promise constructor in a separate, non-async function. Then await the non-async function from the async function.

function wrapSomeApi() {
  return new Promise(...);
}

async function myAsyncFunction() {
  await wrapSomeApi();
}

When using new Promise(...), the promise must be returned before the work is done

Your code should roughly follow this pattern:

function MyAsyncWrapper() {
  return new Promise((resolve, reject) => {
    const workDoer = new WorkDoer();
    workDoer.on('done', result => resolve(result));
    workDoer.on('error', error => reject(error));
    // exits right away while work completes in background
  })
}

You almost never want to use Promise.resolve(value) or Promise.reject(error). Those are only for cases where you have an interface that needs a promise but you already have the value.

AbortController is for fetch only

The folks that run TC39 have been trying to figure out cancellation for a while, but right now there's no official cancellation API.

AbortController is accepted by fetch for cancelling HTTP requests, and that is useful. But it's not meant for cancelling regular old work.

Luckily, you can do it yourself. Everything with async/await is a co-routine, there's no pre-emptive multitasking where you can abort a thread or force a rejection. Instead, you can create a simple token object and pass it to your long running async function:

const token = { cancelled: false }; 
await doLongRunningTask(params, token); 

To do the cancellation, just change the value of cancelled.

someElement.on('click', () => token.cancelled = true); 

Long running work usually involves some kind of loop. Just check the token in the loop, and exit the loop if it's cancelled

async function doLongRunningTask(params: string, token: { cancelled: boolean }) {
  for (const task of workToDo()) {
    if (token.cancelled)
      throw new Error('task got cancelled');
    await task.doStep();
  }
}

Since you're using react, you need token to be the same reference between renders. So, you can use the useRef hook for this:

function useCancelToken() {
  const token = useRef({ cancelled: false });
  const cancel = () => token.current.cancelled = true;
  return [token.current, cancel];
}

const [token, cancel] = useCancelToken();

// ...

return <>
  <button onClick={ () => doLongRunningTask(token) }>Start work</button>
  <button onClick={ () => cancel() }>Cancel</button>
</>;

hash-wasm is only semi-async

You mentioned you were using hash-wasm. This library looks async, as all its APIs return promises. But in reality, it's only await-ing on the WASM loader. That gets cached after the first run, and after that all the calculations are synchronous.

Even if it is wrapped in an async function or in a function returning a Promise, code must yield the thread to act concurrently, which hash-wasm does not appear to do in its main computation loop.

So how can you let your code breath if you've got CPU intensive code like what hash-wasm uses? You can do your work in increments, and schedule those increments with setTimeout:

for (const step of stepsToDo) {
  if (token.cancelled)
    throw new Error('task got cancelled');

  // schedule the step to run ASAP, but let other events process first
  await new Promise(resolve => setTimeout(resolve, 0));

  const chunk = await loadChunk();
  updateHash(chunk);
}

(Note that I'm using a Promise constructor here, but awaiting immediately instead of returning it)

The technique above will run slower than just doing the task. But by yielding the thread, stuff like React updates can execute without an awkward hang.

If you really need performance, check out Web Workers, which let you do CPU-heavy work off-thread so it doesn't block the main thread. Libraries like workerize can help you convert async functions to run in a worker.


That's everything I have for now, I'm sorry for writing a novel

Answer from parktomatomi on Stack Overflow
🌐
Medium
medium.com › codex › resilient-fetch-requests-in-javascript-with-abortcontroller-a-guide-with-react-examples-573dba8a3758
Resilient Fetch Requests in JavaScript with AbortController: A Guide with React Examples | by Tawan | CodeX | Medium
April 15, 2023 - And when you want to abort the request, simply call controller.abort(). This will cause the promise returned by fetch() to reject with an AbortError. Sure, here’s an example of how to use AbortController with a React input:
🌐
Medium
medium.com › @devxprite › using-abortcontroller-with-fetch-api-and-reactjs-70c4a75c99e2
Using AbortController with Fetch API and ReactJS | by Prateek Singh | Medium
February 17, 2025 - Handling Errors: If a request is aborted, Fetch throws an AbortError. We ignore it because it’s intentional. Real errors (like network failures) are logged for debugging. Avoid Memory Leaks: Always abort requests when a component unmounts. React’s cleanup function in useEffect is perfect for this. Debounce for Extra Smoothness: Combine AbortController with a debounce function (like a 300ms delay) to avoid spamming the API on every keystroke.
Discussions

How to use the AbortController to cancel Promises in React?
Wow! Thank you for this detailed answer. I removed the AbortController. There are some posts that not only use the controller with fetch, but in my case it just didn't work. I then created my own token as you described - now it works :). More on stackoverflow.com
🌐 stackoverflow.com
AbortController usage
It depends as always. But if you have some expensive calls to your server the AbortController can really help to minimize server load. Why should you let an stale request run? Also what if your request would update state with stale data because your user is clicking through some filter options or something? I would say AbortController is perfect for this type of scenarios. More on reddit.com
🌐 r/reactjs
5
3
October 2, 2022
reactjs - React - using AbortController on every request as a custom hook - Stack Overflow
import { useEffect } from 'react'; export const useAbortController = (fetcher,args,dependencies) => { useEffect(() => { const abortController = new AbortController(); const signal = abortController.signal; // fetch here. More on stackoverflow.com
🌐 stackoverflow.com
Calling AbortController.abort() on resolved fetch
I have a React app that can makes a series of fetch requests, depending on user interactions. I want to abort old fetch requests any time the app receives a new one. To accomplish this, I've creat... More on stackoverflow.com
🌐 stackoverflow.com
🌐
j-labs
j-labs.pl › home › tech blog › how to use the useeffect hook with the abortcontroller
AbortController in React. How to use the useEffect hook with the AbortControl? | j‑labs
December 9, 2025 - The useEffect hook in React allows us to perform side effects, such as fetching data, when the component mounts, updates, or unmounts. By integrating AbortController with useEffect, we can cancel ongoing requests when the component unmounts ...
Top answer
1 of 3
22

Based off your code, there are a few corrections to make:

Don't return new Promise() inside an async function

You use new Promise if you're taking something event-based but naturally asynchronous, and wrap it into a Promise. Examples:

  • setTimeout
  • Web Worker messages
  • FileReader events

But in an async function, your return value will already be converted to a promise. Rejections will automatically be converted to exceptions you can catch with try/catch. Example:

async function MyAsyncFunction(): Promise<number> {
  try {
    const value1 = await functionThatReturnsPromise(); // unwraps promise 
    const value2 = await anotherPromiseReturner();     // unwraps promise
    if (problem)
      throw new Error('I throw, caller gets a promise that is eventually rejected')
    return value1 + value2; // I return a value, caller gets a promise that is eventually resolved
  } catch(e) {
    // rejected promise and other errors caught here
    console.error(e);
    throw e; // rethrow to caller
  }
}

The caller will get a promise right away, but it won't be resolved until the code hits the return statement or a throw.

What if you have work that needs to be wrapped with a Promise constructor, and you want to do it from an async function? Put the Promise constructor in a separate, non-async function. Then await the non-async function from the async function.

function wrapSomeApi() {
  return new Promise(...);
}

async function myAsyncFunction() {
  await wrapSomeApi();
}

When using new Promise(...), the promise must be returned before the work is done

Your code should roughly follow this pattern:

function MyAsyncWrapper() {
  return new Promise((resolve, reject) => {
    const workDoer = new WorkDoer();
    workDoer.on('done', result => resolve(result));
    workDoer.on('error', error => reject(error));
    // exits right away while work completes in background
  })
}

You almost never want to use Promise.resolve(value) or Promise.reject(error). Those are only for cases where you have an interface that needs a promise but you already have the value.

AbortController is for fetch only

The folks that run TC39 have been trying to figure out cancellation for a while, but right now there's no official cancellation API.

AbortController is accepted by fetch for cancelling HTTP requests, and that is useful. But it's not meant for cancelling regular old work.

Luckily, you can do it yourself. Everything with async/await is a co-routine, there's no pre-emptive multitasking where you can abort a thread or force a rejection. Instead, you can create a simple token object and pass it to your long running async function:

const token = { cancelled: false }; 
await doLongRunningTask(params, token); 

To do the cancellation, just change the value of cancelled.

someElement.on('click', () => token.cancelled = true); 

Long running work usually involves some kind of loop. Just check the token in the loop, and exit the loop if it's cancelled

async function doLongRunningTask(params: string, token: { cancelled: boolean }) {
  for (const task of workToDo()) {
    if (token.cancelled)
      throw new Error('task got cancelled');
    await task.doStep();
  }
}

Since you're using react, you need token to be the same reference between renders. So, you can use the useRef hook for this:

function useCancelToken() {
  const token = useRef({ cancelled: false });
  const cancel = () => token.current.cancelled = true;
  return [token.current, cancel];
}

const [token, cancel] = useCancelToken();

// ...

return <>
  <button onClick={ () => doLongRunningTask(token) }>Start work</button>
  <button onClick={ () => cancel() }>Cancel</button>
</>;

hash-wasm is only semi-async

You mentioned you were using hash-wasm. This library looks async, as all its APIs return promises. But in reality, it's only await-ing on the WASM loader. That gets cached after the first run, and after that all the calculations are synchronous.

Even if it is wrapped in an async function or in a function returning a Promise, code must yield the thread to act concurrently, which hash-wasm does not appear to do in its main computation loop.

So how can you let your code breath if you've got CPU intensive code like what hash-wasm uses? You can do your work in increments, and schedule those increments with setTimeout:

for (const step of stepsToDo) {
  if (token.cancelled)
    throw new Error('task got cancelled');

  // schedule the step to run ASAP, but let other events process first
  await new Promise(resolve => setTimeout(resolve, 0));

  const chunk = await loadChunk();
  updateHash(chunk);
}

(Note that I'm using a Promise constructor here, but awaiting immediately instead of returning it)

The technique above will run slower than just doing the task. But by yielding the thread, stuff like React updates can execute without an awkward hang.

If you really need performance, check out Web Workers, which let you do CPU-heavy work off-thread so it doesn't block the main thread. Libraries like workerize can help you convert async functions to run in a worker.


That's everything I have for now, I'm sorry for writing a novel

2 of 3
0

I can suggest my library (use-async-effect2) for managing the cancellation of asynchronous tasks/promises. Here is a simple demo with nested async function cancellation:

    import React, { useState } from "react";
    import { useAsyncCallback } from "use-async-effect2";
    import { CPromise } from "c-promise2";
    
    // just for testing
    const factorialAsync = CPromise.promisify(function* (n) {
      console.log(`factorialAsync::${n}`);
      yield CPromise.delay(500);
      return n != 1 ? n * (yield factorialAsync(n - 1)) : 1;
    });
    
    function TestComponent({ url, timeout }) {
      const [text, setText] = useState("");
    
      const myTask = useAsyncCallback(
        function* (n) {
          for (let i = 0; i <= 5; i++) {
            setText(`Working...${i}`);
            yield CPromise.delay(500);
          }
          setText(`Calculating Factorial of ${n}`);
          const factorial = yield factorialAsync(n);
          setText(`Done! Factorial=${factorial}`);
        },
        { cancelPrevious: true }
      );
    
      return (
        <div>
          <div>{text}</div>
          <button onClick={() => myTask(15)}>
            Run task
          </button>
          <button onClick={myTask.cancel}>
            Cancel task
          </button>
        </div>
      );
    }
🌐
MDN Web Docs
developer.mozilla.org › en-US › docs › Web › API › AbortController › abort
AbortController: abort() method - Web APIs - MDN Web Docs
September 17, 2025 - The abort() method of the AbortController interface aborts an asynchronous operation before it has completed. This is able to abort fetch requests, the consumption of any response bodies, or streams.
🌐
Westbrookdaniel
westbrookdaniel.com › blog › react-abort-controllers
Using AbortControllers to Cancel Fetch in React - Daniel Westbrook
So let's fix it by canceling the fetch. useEffect(() => { const controller = new AbortController(); fetch("https://jsonplaceholder.typicode.com/posts/1", { signal: controller.signal, }) .then((res) => res.json()) .then((json) => setMessage(json.title)) .catch((error) => console.error(error.message)); return () => controller.abort(); }, []);
Find elsewhere
🌐
The New Stack
thenewstack.io › home › cancel asynchronous react app requests with abortcontroller
Cancel Asynchronous React App Requests with AbortController - The New Stack
April 24, 2024 - AbortController creates a signal that can be passed to the Fetch API or other APIs that support terminating web requests. You can call the abort method on the AbortController instance when you want to stop the ongoing operation.
🌐
YouTube
youtube.com › colby fayock
Abort Fetch API Requests using AbortController - YouTube
Learn how to use the AbortController in JavaScript to cancel API requests using fetch in React.We'll walk through how to set up the AbortController with a fe...
Published   October 12, 2023
Views   9K
🌐
DEV Community
dev.to › bil › using-abortcontroller-with-react-hooks-and-typescript-to-cancel-window-fetch-requests-1md4
Using AbortController (with React Hooks and TypeScript) to cancel window.fetch requests - DEV Community
March 27, 2019 - const abortController = new AbortController() const promise = window .fetch('https://api.example.com/v1/me', { headers: {Authorization: `Bearer [my access token]`}, method: 'GET', mode: 'cors', signal: abortController.signal, }) .then(res => res.json()) .then(res => { console.log(res.me) }) .catch(err => { console.error('Request failed', err) }) // Cancel the request if it takes more than 5 seconds setTimeout(() => abortController.abort(), 5000)
🌐
Medium
pgarciacamou.medium.com › using-abortcontroller-with-fetch-api-and-reactjs-8d4177e51270
Using AbortController with Fetch API and ReactJS. | by Pablo Garcia | Medium
June 3, 2022 - The trick relies on having an internal Map of all the different AbortControllers so that we can abort them at any time manually using abortRequestSafe by passing the unique id (when a user clicks a button, etc) or automatically every time you call fetchRequest with the same unique ID because the previous AbortController will be aborted and replaced by a new controller using abortAndGetSignalSafe. We can use this useComponentWillUnmount react hook:
🌐
Stack Overflow
stackoverflow.com › questions › 72491331 › calling-abortcontroller-abort-on-resolved-fetch
Calling AbortController.abort() on resolved fetch
Fetch returns a promise so, once it resolves, any abortion does not have any effects. You can run this simple demo in the console, you'll see that the request completes (at least until connection) and no errors are generated: async function ...
🌐
Wanago
wanago.io › home › using abortcontroller to deal with race conditions in react
Using AbortController to deal with race conditions in React
April 11, 2022 - With it, we can abort one or more fetch requests. To do this, we need to create an instance of the AbortController and use it when making the fetch request.
🌐
Medium
eminfurkan.medium.com › managing-asynchronous-operations-with-abortcontroller-in-react-e9bec3565ec8
Managing Asynchronous Operations with AbortController in React | by Furkan Tezeren | Medium
July 30, 2023 - The returned function inside useEffect is used to cancel the operation using the abortController when the component unmounts. This way, when the component unmounts, we can cancel the fetch operation, preventing any unwanted operations from running and ensuring better control over our asynchronous tasks. Lastly, Let’s create a custom React hook called useAbortableFetch to handle asynchronous HTTP requests with the AbortController.
🌐
LogRocket
blog.logrocket.com › home › the complete guide to the abortcontroller api
The complete guide to the AbortController API - LogRocket Blog
March 12, 2025 - This tutorial will offer a complete guide on how to use the AbortController and AbortSignal APIs in both your backend and frontend. In our case, we’ll focus on Node.js and React.
🌐
MDN Web Docs
developer.mozilla.org › en-US › docs › Web › API › AbortController
AbortController - Web APIs - MDN Web Docs
September 17, 2025 - Creates a new AbortController object instance. ... Returns an AbortSignal object instance, which can be used to communicate with, or to abort, an asynchronous operation. ... Aborts an asynchronous operation before it has completed. This is able to abort fetch requests, consumption of any response ...
🌐
Localcan
localcan.com › blog › abortcontroller-nodejs-react-complete-guide-examples
AbortController in Node.js and React - Complete Guide with 5 Examples - Blog - LocalCan™
May 23, 2025 - Learn how AbortController can cancel fetch requests, event listeners, streams, child processes in JavaScript. Stop memory leaks and improve UX with examples
🌐
DEV Community
dev.to › leapcell › do-you-really-know-abortcontroller-3628
Do You Really Know AbortController? - DEV Community
January 16, 2025 - For older browsers, consider adding a polyfill to support AbortController. In React, effects can inadvertently run in parallel if the component updates before a previous asynchronous task completes: function FooComponent({ something }) { useEffect(async () => { const data = await fetch(url + something); // Handle the data }, [something]); return <>...</>; }