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 - In case you didn’t know, browsers support an API called AbortController, which is typically used to cancel ongoing fetch requests. More info always available at MDN. ... An example using React’s useEffect.
🌐
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 AbortController interface provides a way to cancel ongoing asynchronous operations. When combined with React's useEffect hook, AbortController allows us to effectively manage asynchronous requests and prevent memory leaks.
🌐
Medium
medium.com › @rakeshraj2097 › efficient-request-handling-in-react-with-axios-and-abortcontroller-e47bafab87c9
Efficient Request Handling in React with Axios and AbortController | by Rakeshraj | Medium
June 12, 2024 - The AbortController interface represents a controller object that allows you to abort one or more DOM requests as and when desired.
🌐
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 - React query was designed to abstract away these lower-level details so you don’t have to handle cancellation manually. It automatically cancels and retries queries under various conditions, including when the component unmounts, the query key changes (like when `userId` changes), or even when the app comes back into focus. So, unless you need custom behavior, you don’t need to explicitly define an AbortController with React Query — React Query already uses one under the hood.
🌐
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 - The following code creates a ref for the abortController and initializes it with an empty string. This prevents React from rerendering the component when the ref value changes. For each API request, it creates a new abortController and passes its signal property (a read-only property of the abortController that can be used to communicate with the request) to the request.
Find elsewhere
🌐
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.
🌐
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.
🌐
DEV Community
dev.to › leapcell › do-you-really-know-abortcontroller-3628
Do You Really Know AbortController? - DEV Community
January 16, 2025 - Many developers might think they understand AbortController, but its capabilities go far beyond the basics. From canceling fetch requests to managing event listeners and React hooks.
🌐
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 is like a stop button for your JavaScript code. It lets you cancel things that are running - like API calls, streams, or event listeners. Personally, after years of coming up with overly complex and tedious ways to remove listeners ...
🌐
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 - // src/hooks/useProfileInformation.jsx import {useState, useEffect} from 'react' export function useProfileInformation({accessToken}) { const [profileInfo, setProfileInfo] = useState(null) useEffect(() => { const abortController = new AbortController() window .fetch('https://api.example.com/v1/me', { headers: {Authorization: `Bearer ${accessToken}`}, method: 'GET', mode: 'cors', signal: abortController.signal, }) .then(res => res.json()) .then(res => setProfileInfo(res.profileInfo)) return function cancel() { abortController.abort() } }, [accessToken]) return profileInfo } // src/app.jsx impor
🌐
GitHub
github.com › facebook › react › issues › 25962
Bug: React.StrictMode causes AbortController to cancel · Issue #25962 · facebook/react
January 5, 2023 - import { useEffect, useState } from "react"; import axios from "axios"; export default function App() { const [data, setData] = useState([]); const [dataLoading, setDataLoading] = useState(false); const controller = new AbortController(); const { signal } = controller; const getData = async () => { console.log("getData API call"); setDataLoading(true); return axios .get(`/api/v1/health`, { signal }) .then((res) => { setData(["data: ", "loaded", " successfully"]); }) .catch((err) => { console.log("getData caught error"); console.log({ err }); }) .finally(() => setDataLoading(false)); }; useEffect(() => { getData(); return () => { console.log("CONTROLLER ABORT"); controller.abort(); }; }, []); const refreshData = async () => { await getData(); }; return ( <div> {data.length ?
Author   v3rron
🌐
MDN Web Docs
developer.mozilla.org › en-US › docs › Web › API › AbortController › abort
AbortController: abort() method - Web APIs | MDN - Mozilla
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 - AbortController is a global class in JavaScript that you can use to abort, well, anything! ... The signal property, which is an instance of AbortSignal. This is a pluggable part you can provide to any API to react to an abort event, and implement it accordingly...
🌐
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 }
🌐
YouTube
youtube.com › shorts › Z09xJq5iA0c
Every React Dev Needs To Know This About AbortController - YouTube
Full Video: https://youtu.be/BeZfiCPhZbI🌎 Find Me Here:My Blog: https://blog.webdevsimplified.comMy Courses: https://courses.webdevsimplified.comPatreon: ht...
Published   March 13, 2025
🌐
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.
🌐
Hamon
hamon.in › blog › mastering-abortcontroller-in-react-canceling-requests-made-simple
Mastering AbortController in React: Canceling Requests ...
AbortController is a small but mighty tool for React developers. It keeps your app performant by canceling unnecessary requests and prevents those pesky “Can’t set state on an unmounted component” errors.