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
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>
      );
    }
🌐
Medium
medium.com › @icjoseph › using-react-to-understand-abort-controllers-eb10654485df
Using React to understand Abort Controllers | by Joseph Chamochumbi | Medium
November 18, 2022 - When the abort method on the controller is called, the fetch operation and every subsequent, then methods are discarded, and the catch method is executed. If you are setting an error state inside your catch block, then you need to differentiate ...
Discussions

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
Abort Controller question
It’s probably just good practice to remove any existing errors if you have a successful response to clean up the state. I’m not sure if it’s possible but because everything is asynchronous, I feel like with the right timing you can potentially have a request resolve as an error after the next request starts. More on reddit.com
🌐 r/reactjs
5
1
August 30, 2025
Using AbortController to remove event listeners seems to remove them permanently?
I question what you are trying to do. If this is for learning for the sake of learning a new thing, carry on. If you think controlling events and event handling is fine this way, I encourage you to find alternative sources. Abort controller is meant to interrupt a promise, particularly for the fetch API. You seem to be using it to interrupt an event handler? Which seems odd. Events and their handlers are meant to be very fast. It doesn't jive with my understanding of js development, and nothing here seems right to me... So.. I ask again - what are you trying to do? More on reddit.com
🌐 r/learnjavascript
9
4
January 1, 2024
How to cancel a request in both front and back?
An endpoint that takes a few minutes? Sounds like this should be done as a task and have the front end know when it’s completed The front end would initiate the task then let the back end know when that task should be cancelled. Your back end would have to know how to cancel it. (Eg kill a thread or unwind a transaction etc) More on reddit.com
🌐 r/reactjs
9
8
August 25, 2023
🌐
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 - AbortController fixes all these issues with one simple pattern. This is the most common use case. When your React component unmounts, you want to remove all event listeners. JSCopy · function Component() { useEffect(() => { const controller = new AbortController() const signal = controller.signal // Add multiple event listeners document.addEventListener('click', handleClick, { signal }) document.addEventListener('keydown', handleKeydown, { signal }) window.addEventListener('resize', (e) => console.log('Window resized!'), { signal }) function handleClick(e) { console.log('Clicked!') } function handleKeydown(e) { console.log('Key pressed!') } // Cleanup all listeners at once return () => controller.abort() }, []) return <div>My Component</div> } Clean and simple.
🌐
JavaScript in Plain English
javascript.plainenglish.io › abortcontroller-and-why-we-even-need-it-fe7ec8465886
AbortController and why we even need it. | by Akilesh Rao | JavaScript in Plain English
November 4, 2024 - By doing this, I can now get rid of all the useStates because it’s handled by react query and the useEffect can also be removed. I also need to refactor the JSX because the loading and error states now have a different name. <div> {isPending && <p>Loading…</p>} {isError && <p>Error: {error.message}</p>} {user && ( <> <h1>{user.name}</h1> <p>Email: {user.email}</p> </> )} </div> And that should do it. We have Tanstack query in place. Now to add abort controller to this, all we have to do is use the signal that we get out of the box from the query function’s params, pass it to the fetchUser function and then use it as part of the options, just like we did earlier.
🌐
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.
🌐
Kettanaito
kettanaito.com › blog › dont-sleep-on-abort-controller
Don't Sleep on AbortController - kettanaito.com
September 17, 2024 - Every abort event is accompanied with the reason for that abort. That yields even more customizability as you can react to different abort reasons differently. The abort reason is an optional argument to the controller.abort() method.
🌐
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 - It also adds an onClick event listener for the cancel button, which calls the controller’s abort() method to cancel the request. AbortController is a powerful tool for managing asynchronous requests in React applications and can help improve ...
Find elsewhere
🌐
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 - Inside the cleanup function, we have called abort() on the controller to cancel the request. By integrating AbortController with useEffect, we ensure that any ongoing asynchronous operation is properly canceled when it is no longer needed.
🌐
Westbrookdaniel
westbrookdaniel.com › blog › react-abort-controllers
Using AbortControllers to Cancel Fetch in React - Daniel Westbrook
useEffect(() => { const controller ... }, []); Here we use the web api AbortController as the signal for fetch. By returning a function from useEffect we can trigger the abort controller on dismount (see the React docs)....
🌐
LogRocket
blog.logrocket.com › home › the complete guide to the abortcontroller api
The complete guide to the AbortController API - LogRocket Blog
March 12, 2025 - You pass in the controller’s signal property to the API, and it aborts the process when you invoke the controller’s abort method. However, to implement a custom cancelable promise-based functionality, you need to add an event listener which listens for the abort event and cancels the process from the event handler when the event is triggered. Editor’s note: This article was updated by Chizaram Ken in March 2025 to include more comprehensive information on frontend and backend use cases for AbortController in both Node.js and React.
🌐
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 - It is something particularly useful, especially to adhere to React's Lifecycle, and even more so with the introduction of React Hooks. Thankfully, we have something called AbortController! 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) As you should expect, this will cancel the request after 5 seconds.
🌐
Mew's Blog
mewis.me › blog › reactjs › consider-using-abortcontroller-in-javascript-and-react
Consider using AbortController in JavaScript and React - Mew's Portfolio
July 6, 2025 - const controller = new AbortController(); const { signal } = controller; // Using the signal in a fetch request fetch('https://api.example.com/data', { signal }) .then(response => response.json()) .then(data => console.log(data)) .catch(error => { if (error.name === 'AbortError') { console.log('Fetch request canceled'); } else { console.error('Fetch error:', error); } }); // To cancel the fetch request controller.abort(); In React, it's common to initiate fetch requests in useEffect to fetch data when a component mounts.
🌐
GitHub
gist.github.com › kentcdodds › b36572b6e9227207e6c71fd80e63f3b4
abort-controller.js · GitHub
function useAbortController() { const abortControllerRef = React.useRef<AbortController>() const getAbortController = () => { return (abortControllerRef.current = abortControllerRef.current || new AbortController()) } React.useEffect(() => { return () => getAbortController().abort() }, []) const getSignal = React.useCallback(() => getAbortController().signal, []) return getSignal }
🌐
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 - This article explains what race conditions are and provides a solution using the AbortController.
🌐
Reddit
reddit.com › r/reactjs › abort controller question
r/reactjs on Reddit: Abort Controller question
August 30, 2025 -
useEffect(
    function () {
      const controller = new AbortController();

      async function fetchMovies() {
        try {
          setIsLoading(true);
          setError("");

          const res = await fetch(
            `http://www.omdbapi.com/?apikey=${KEY}&s=${query}`,
            { signal: controller.signal }
          );

          if (!res.ok)
            throw new Error("Something went wrong with fetching movies");

          const data = await res.json();
          if (data.Response === "False") throw new Error("Movie not found");

          setMovies(data.Search);
          setError("");
        } catch (err) {
          if (err.name !== "AbortError") {
            console.log(err.message);
            setError(err.message);
          }
        } finally {
          setIsLoading(false);
        }
      }

      if (query.length < 3) {
        setMovies([]);
        setError("");
        return;
      }

      handleCloseMovie();
      fetchMovies();

      return function () {
        controller.abort();
      };
    },
    [query]
  );

I was following a tutorial to make movie search app and as you can see in this useEffect I am using an abortController to prevent race conditions. However the part I'm confused is the when the instructor says we need to add a setError("") after a successful request. He does not really explain why and I'm thinking it is redundant since we already have the abort controller and the setError("") in the beginning of the request. Anybody knows why he said to add one after the request? Maybe soon edge case that I can't think of?

🌐
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 - import { useEffect, useState } from 'react'; function MyComponent() { const [data, setData] = useState(null); const controller = new AbortController(); const signal = controller.signal; useEffect(() => { async function fetchData() { try { const response = await fetch('https://my-api.com/endpoint', { signal }); const json = await response.json(); setData(json); } catch (error) { if (error.name === 'AbortError') { console.log('Fetching data was aborted'); } else { console.error(error); } } } fetchData(); return () => { controller.abort(); }; }, []); return ( <div> {data ?
🌐
MDN Web Docs
developer.mozilla.org › en-US › docs › Web › API › AbortController
AbortController - Web APIs - MDN Web Docs
September 17, 2025 - Aborts an asynchronous operation before it has completed. This is able to abort fetch requests, consumption of any response bodies, and streams.