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
doSomeStuff
called? Are you sure that it is not being called prior to you setting the abort signal? – Coreawait
,Worker
, or some other means, it won't actually be async. It will just do the work and return a promise that's already resolved. – Khmerawait hashChunk(chunk, hasher);
is called." - please add that code (and optimally, even the implementation ofhashChunk
), otherwise this cannot really be solved. – UnknitAbortController
in aReact.useRef
or something so that it's not creating a new one on every render. And then if you want to allow the user to try again, you would need to make a newAbortController
for the next try. – Josefinejoseito