I want to cancel a promise in my React application using the AbortController and unfortunately the abort event is not recognized so that I cannot react to it.
My setup looks like this:
WrapperComponent.tsx: Here I'm creating the AbortController and pass the signal to my method calculateSomeStuff that returns a Promise. The controller I'm passing to my Table component as a prop.
export const WrapperComponent = () => {
const controller = new AbortController();
const signal = abortController.signal;
// This function gets called in my useEffect
// I'm passing signal to the method calculateSomeStuff
const doSomeStuff = (file: any): void => {
calculateSomeStuff(signal, file)
.then((hash) => {
// do some stuff
})
.catch((error) => {
// throw error
});
};
return (<Table controller={controller} />)
}
The calculateSomeStuff method looks like this:
export const calculateSomeStuff = async (signal, file): Promise<any> => {
if (signal.aborted) {
console.log('signal.aborted', signal.aborted);
return Promise.reject(new DOMException('Aborted', 'AbortError'));
}
for (let i = 0; i <= 10; i++) {
// do some stuff
}
const secret = 'ojefbgwovwevwrf';
return new Promise((resolve, reject) => {
console.log('Promise Started');
resolve(secret);
signal.addEventListener('abort', () => {
console.log('Aborted');
reject(new DOMException('Aborted', 'AbortError'));
});
});
};
Within my Table component I call the abort() method like this:
export const Table = ({controller}) => {
const handleAbort = ( fileName: string) => {
controller.abort();
};
return (
<Button
onClick={() => handleAbort()}
/>
);
}
What am I doing wrong here? My console.logs are not visible and the signal is never set to true after calling the handleAbort handler.
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.
Async code that doesn't actually await doesn't have any benefits. It will not pause to unblock the thread.
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
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>
);
}
Related
Promises just baffle me.
I'm trying to make a mock data service to imitate axios.
My mock put call passes a targetUrl to _fetch which then sees if it's a valid url and either returns a new Promise with a delayed .resolve
const _returnResponse = (mockData, time = 0) => new Promise((resolve) => {
setTimeout(() => {
resolve(mockData);
}, time);
});
or a new Promise with a delayed .reject
const _returnError = (time = simulatedDelay) => {
const returnValue = new Promise(((resolve, reject) => {
setTimeout(() => {
reject(new Error('error'));
}, time);
}));
return returnValue;
};
but when I make my mock put call this returns a mock data that the calling method interprets as a success and console logs in its .then
put(target, putBody) {
const returnValue = _fetch(target, simulatedDelay)
returnValue.then(response => _console('PUT', target, response, putBody));
return returnValue;
},
But with an invalid target console logs an uncaught error
or this handles the error correctly, but console logs an undefined response
put(target, putBody) {
const returnValue = _fetch(target, simulatedDelay).then(response => _console('PUT', target, response, putBody));
return returnValue;
},
Here's the calling method:
saveStuff({ commit, state }, newStuff) {
//other code
return this.$mockAxios.put(url, putBody)
.then((response) => {
return response;
});
},
I feel like I'm completely missing something and I've researched this for hours and I'm still not getting it.
As a direct answer to the question: yes, you can add .then() to a promise after it's created.
Example:
const hi = new Promise((resolve, reject) => {
setTimeout(() => resolve('hello'), 2000);
});
hi.then(result => console.log(result));
As for promises baffling you, I would recommend (aside from more reading) just playing around a lot in your IDE with setTimeout. I know you're already using setTimeout in your mock, but strip it down further and just run the code in its own file, so that you control the whole environment. With lots of console.log('blah') to see the order and such. Also ensure you're just as familiar with callbacks, as promises are just syntactic sugar for callbacks. Try to read up on the JS event loop at the same time - it might provide some context if you know how and when a callback/promise executes.
Note that you can even add .then() after the callback has resolved or rejected. Hence the term "promise" - it's literally a promise that your .then() function will run. https://en.wikipedia.org/wiki/Promise_theory
const hi = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('one second');
resolve();
}, 1000);
});
setTimeout(() => {
hi.then(() => console.log('two seconds. this executes approximately a full second after the first promise has resolved'));
}, 2000);
I'm quite a newbie in JavaScript and in Promises.
I'm trying to build an array of objects that I get from an API.
To do so, I've build two functions in a file MyFile.js.
The first one returns a promise when an axios promise is resolved. It's
function get_items (url) {
return new Promise((resolve, reject) => {
let options = {
baseURL: url,
method: 'get'
}
axios(options)
.then(response => {
resolve(response.data)
})
.catch(error => {
reject(error.stack)
})
})
}
The second one looks like this:
let output = []
let next_url = 'https://some_url.com/api/data'
async function get_data () {
try {
let promise = new Promise((resolve, reject) => {
if (next_url) {
get_items(next_url)
.then(response => {
output.push(...response.results)
if (response.next) {
next_url = response.next
console.log('NEXT_URL HERE', next_url)
get_data()
} else {
console.log('else')
next_url = false
get_data()
}
})
.catch(error => {
reject(error.stack)
})
} else {
console.log('before resolve')
resolve(output)
}
})
return await promise
} catch(e) {
console.log(e)
}
}
It's where I'm grinding my teeth.
What I think I understand of this function, is that:
it's returning the value of a promise (that's what I understand return await promise is doing)
it's a recursive function. So, if there is a next_url, the function continues on. But if there is not, it gets called one last time to go into the else part where it resolves the array output which contains the results (values not state) of all the promises. At least, when I execute it, and check for my sanity checks with the console.log I wrote, it works.
So, output is filled with data and that's great.
But, when I call this function from another file MyOtherFile.js, like this:
final_output = []
MyFile.get_data()
.then(result => {
console.log('getting data')
final_output.push(...result)
})
it never gets into the then part. And when I console.log MyFile.get_data(), it's a pending promise.
So, what I would like to do, is be able to make get_data() wait for all the promises result (without using Promise.all(), to have calls in serie, not in parallel, that would be great for performances, I guess?) and then be able to retrieve that response in the then part when calling this function from anywhere else.
Keep in mind that I'm really a newbie in promises and JavaScript in general (I'm more of a Python guy).
Let me know if my question isn't clear enough.
I've been scratching my head for two days now and it feels like I'm running in circle.
Thanks for being an awesome community!
This is a bit untested
const api_url = 'https://some_url.com/api/data';
get_data(api_url).then((results) => {
console.log(results);
}).catch((error) => {
// console.error(error);
});
function get_items (url) {
const options = {
baseURL: url,
method: 'get'
};
return axios(options).then((response) => response.data);
}
async function get_data(next_url) {
const output = [];
while (next_url) {
const { results, next } = await get_items(next_url);
output.push(...results);
next_url = next;
}
return output;
}
Basically it makes things a bit neater. I suggest to look at more examples with Promises and the advantage and when to ease await/async. One thing to keep in mind, if you return a Promise, it will follow the entire then chain, and it will always return a Promise with a value of the last then.. if that makes sense :)
There are a few problems. One is that you never resolve the initial Promise unless the else block is entered. Another is that you should return the recursive get_data call every time, so that it can be properly chained with the initial Promise. You may also consider avoiding the explicit promise construction antipattern - get_items already returns a Promise, so there's no need to construct another one (same for the inside of get_items, axios calls return Promises too).
You might consider a plain while loop, reassigning the next_url string until it's falsey:
function get_items (baseURL) {
const options = {
baseURL: url,
method: 'get'
}
// return the axios call, handle errors in the consumer instead:
return axios(options)
.then(res => res.data)
}
async function get_data() {
const output = []
let next_url = 'https://some_url.com/api/data'
try {
while (next_url) {
const response = await get_items(next_url);
output.push(...response.results)
next_url = response.next;
}
} catch (e) {
// handle errors *here*, perhaps
console.log(e)
}
return output;
}
Note that .catch will result in a Promise being converted from a rejected Promise to a resolved one - you don't want to .catch everywhere, because that will make it difficult for the caller to detect errors.
Another way of doing it is to not use async at all and just recursively return a promise:
const getItems = (url) =>
axios({
baseURL: url,
method: 'get',
}).then((response) => response.data);
const getData = (initialUrl) => {
const recur = (result, nextUrl) =>
!nextUrl
? Promise.resolve(result)
: getItems(nextUrl).then((data) =>
recur(result.concat([data.results]), data.next),
);
return recur([],initialUrl)
.catch(e=>Promise.reject(e.stack));//reject with error stack
};
As CertainPerformance noted; you don't need to catch at every level, if you want getData to reject with error.stack you only need to catch it once.
However; if you had 100 next urls and 99 of them were fine but only the last one failed would you like to reject in a way that keeps the results so far so you can try again?
If you do then the code could look something like this:
const getData = (initialUrl) => {
const recur = (result, nextUrl) =>
!nextUrl
? Promise.resolve(result)
: getItems(nextUrl)
.catch(e=>Promise.reject([e,result]))//reject with error and result so far
.then((data) =>
recur(result.concat([data.results]), data.next),
);
return recur([],initialUrl);//do not catch here, just let it reject with error and result
};
I have created a long running Promise that I'm wrapping with this simple function that I created to create a watch a Promise race.
The function is below:
export const promiseTimeout = (
promise,
timeoutMs = 10000, //10 secs
message = 'Timeout reached, please try again',
) =>
Promise.race([
promise,
new Promise((resolve, reject) =>
setTimeout(() => {
reject(message);
}, timeoutMs),
),
]);
The way I'm planning to use it is that I would pass the long running Promise that might require other unpredictable resources such as internet, file, system setting, etc.
Usage would be like below:
const result = await promiseTimeout(longRunningFunction())
.catch(err => /* do something with the error , show toast or alert */);;
What is currently happening with this is that whenever the timeout is reached it would call the catch but the operation of the longRunningFunction will still continue.
How can I stop the operations on the passed Promise in argument if ever timeout is reached?
How can I stop the operations on the passed Promise in argument if ever timeout is reached?
Hey, sorry, we don't have cancellation of async functions yet.
Note however that a promise is a value and not an action, once you have the promise given we won't have cancellable promises in JavaScript it is impossible to cancel the action.
The only thing you can do is to do something like the cancellation proposal and write your longRunningFunction with a token:
function longRunningFunction() {
const signal = { requested: false };
async function internal() {
// your regular code here
// whenever you can stop execution:
if(signal.requested) {
return; // and cancel internal operations
}
}
let res = internal();
res.signal = signal;
return res;
}
Then write your race as:
export const promiseTimeout = (
promise,
timeoutMs = 10000, //10 secs
message = 'Timeout reached, please try again',
) =>
Promise.race([
promise,
new Promise((resolve, reject) =>
setTimeout(() => {
reject(message);
if (promise.signal) promise.signal.requested = true;
}, timeoutMs),
),
]);
Let's say I have a simple Node.js app that has these methods in another file:
module.exports = {
completeQuest(data) {
// Code here
},
killMonster(data) {
// Also code here
},
};
And they are not AJAX commands. These just manipulate some data within the app. I'd export as such in allActions.js:
const player = require('./playerActions');
module.exports = {
player,
};
and of course later, in main JS file const actions = require('./allActions');
So, generally, I'd do:
actions.player.killMonster();
actions.player.completeQuest();
But I want them to act one after the other. I know I can do async/await, but I'm not doing any AJAX calls, so would a Promise still be the best way?
What about using a yield function? Would that be good? I'm looking for opinions is all. Thank you.
I'm going to assume that killMonster and completeQuest perform asynchronous actions. (You've said they're "not ajax", but your question suggests they're not synchronous, either.)
Yes, this is a use case for promises, either explicit promises or those provided by async functions. Have killMonster and completeQuest return a promise (either by doing so explicitly or making them async functions), and then either:
actions.player.killMonster()
.then(
() => actions.player.completeQuest()
)
.catch(error => {
// Handle error
});
or, within an async function:
try {
await actions.player.killMonster();
await actions.player.completeQuest();
} catch (error) {
// Handle error
}
Here's a simple async/await example using setTimeout to provide the asynchronous part:
const delay = (ms, ...args) =>
new Promise(resolve => {
setTimeout(resolve, ms, ...args);
});
// Stand-ins for the actions
const player = {
actions: {
async killMonster() {
await delay(500, "monster killed");
},
async completeQuest() {
await delay(800, "quest complete");
}
}
};
// Top-leve async function (see my answer here:
// https://stackoverflow.com/questions/46515764/how-can-i-use-async-await-at-the-top-level
(async () => {
try {
console.log("Killing monster...");
await player.actions.killMonster();
console.log("Monster killed; completing question...");
await player.actions.killMonster();
console.log("Quest complete");
} catch (e) {
// Deal with error (probably don't just dump it to the console like this does)
console.error(e);
}
})();
Let's say I have two async events, both need to i/o with remote exchange.
placeOrder()
cancelOrder()
Both events fire in async way, which means cancelOrder can be called before placeOrder return. Tricky part is I need the placeOrder to return an Order ID first otherwise there is no way to call cancelOrder, so I need some way to block the cancelOrder event right until placeOrder returns, and the blockage cannot be too long otherwise the Order may be executed, so loop/timeout/frequent checking doesn't work here.
Any idea?
You would use a Promise for that. If your functions already return a promise, you can simply chain the both functions using then()
placeOrder().then(val => cancelOrder(val));
If they do not, you can put them inside a new Promise
function foo() {
return new Promise((resolve, reject) => {
// do stuff
resolve('<result of placeOrder here>');
});
}
function bar(val) {
return new Promise((resolve, reject) => {
// do stuff
resolve('whatever')
})
}
and call
foo()
.then(value => bar(value))
.then(console.log);
If you are able to use ES2017, the you can use async functions. For example, I'm going to assume that your functions perform some sort of request to the database using fetch or axios since you haven't specified. Then you can write placeOrder and cancelOrder like so:
const placeOrder = async () => {
try {
const response = await fetch('/place_order');
// Do something with the response
} catch (err) {
// Handle error
}
};
const cancelOrder = async () => {
try {
const response = await fetch('/cancel_order');
// Do something with the response
} catch (err) {
// Handle error
}
};
const someFunction = async () => {
await placeOrder();
await cancelOrder();
};