There has been other topics on SE, but most of them are dated 5 years ago. What is the current, up-to-date approach to cancel await call in JS? i.e.
async myFunc(){
let response = await oneHourLastingFunction();
myProcessData(response);
}
at specific moment application decides it no longer want to wait that oneHourLastingFunction, but it is stuck in "await". How to cancel that? Any standard ways of cancelation-tokens/abortControllers for promises?
Canceling an asynchronous procedure is still not a trivial task, especially when you need deep cancellation and flow control. There is no native solution at the moment. All you can do natively:
pass AbortController instance to each nested async function you want to make cancellable
subscribe all internal micro-tasks (requests, timers, etc) to the signal
optionally unsubscribe completed micro-tasks from the signal
call abort method of the controller to cancel all subscribed micro-tasks
This is quite verbose and a tricky solution with potential memory leaks.
I can just suggest my own solution to this challenge- c-promise2, which provides cancelable promises and a cancelable alternative for ECMA asynchronous functions - generators.
Here is an basic example (Live Demo):
import { CPromise } from "c-promise2";
// deeply cancelable generator-based asynchronous function
const oneHourLastingFunction = CPromise.promisify(function* () {
// optionally just for logging
this.onCancel(() =>
console.log("oneHourLastingFunction::Cancel signal received")
);
yield CPromise.delay(5000); // this task will be cancelled on external timeout
return "myData";
});
async function nativeAsyncFn() {
await CPromise.delay(5000);
}
async function myFunc() {
let response;
try {
response = await oneHourLastingFunction().timeout(2000);
} catch (err) {
if (!CPromise.isCanceledError(err)) throw err;
console.warn("oneHourLastingFunction::timeout", err.code); // 'E_REASON_TIMEOUT'
}
await nativeAsyncFn(response);
}
const nativePromise = myFunc();
Deeply cancellable solution (all functions are cancellable) (Live Demo):
import { CPromise } from "c-promise2";
// deeply cancelable generator-based asynchronous function
const oneHourLastingFunction = CPromise.promisify(function* () {
yield CPromise.delay(5000);
return "myData";
});
const otherAsyncFn = CPromise.promisify(function* () {
yield CPromise.delay(5000);
});
const myFunc = CPromise.promisify(function* () {
let response;
try {
response = yield oneHourLastingFunction().timeout(2000);
} catch (err) {
if (err.code !== "E_REASON_TIMEOUT") throw err;
console.log("oneHourLastingFunction::timeout");
}
yield otherAsyncFn(response);
});
const cancellablePromise = myFunc().then(
(result) => console.log(`Done: ${result}`),
(err) => console.warn(`Failed: ${err}`)
);
setTimeout(() => {
console.log("send external cancel signal");
cancellablePromise.cancel();
}, 4000);
Related
I have read here that unhandled errors might cause cold starts. I am implementing a triggered function, as follows:
exports.detectPostLanguage = functions
.region("us-central1")
.runWith({ memory: "2GB", timeoutSeconds: "540" })
.firestore.document("posts/{userId}/userPosts/{postId}")
.onCreate(async (snap, context) => {
// ...
const language = await google.detectLanguage(post.text);
// ... more stuff if no error with the async operation
});
Do I need to catch the google.detectLanguage() method errors to avoid cold starts?
If yes, what should I do (A or B)?
A:
const language = await google.detectLanguage(post.text)
.catch(err => {
functions.logger.error(err);
throw err; // <---- Will still cause the cold start?
});
B:
try {
var language = await google.detectLanguage(post.text)
} catch(err) {
functions.logger.error(err);
return null; // <----
}
UPDATE
Based on Frank solution:
exports.detectPostLanguage = functions
.region("us-central1")
.runWith({ memory: "2GB", timeoutSeconds: "540" })
.firestore.document("posts/{userId}/userPosts/{postId}")
.onCreate(async (snap, context) => {
try {
// ...
const language = await google.detectLanguage(post.text);
// ... more stuff if no error with the async operation
} catch(err) {
return Promise.reject(err);
}
return null; // or return Promise.resolve();
});
If any exception escapes from the Cloud Function body, the runtime assumes that the container is left in an unstable state and won't schedule it to handle events anymore.
To prevent this, ensure that no exception escapes from your code. You can either return no value, or a promise that indicates when any asynchronous calls in your code are done.
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>
);
}
I have some difficulties with a nested promise (below).
For now I'm using an async function in the catch to trigger authentication Errors.
But is it really a good way and isn't there a better way ?
The function have to send a POST request. If an authentication Error is thrown then login(), else throw the error.
If login() is fulfilled : retry the POST (and then return the results), else throw the error;
function getSomeData() {
return post('mySpecialMethod');
}
function login() {
const params = { /* some params */ }
return post('myLoginMethod', params).then(result => {
/* some processing */
return { success: true };
});
}
const loginError = [1, 101];
function post(method, params, isRetry) {
const options = /* hidden for brevity */;
return request(options)
// login and retry if authentication Error || throw the error
.catch(async ex => {
const err = ex.error.error || ex.error
if (loginError.includes(err.code) && !isRetry) { // prevent infinite loop if it's impossible to login -> don't retry if already retried
await login();
return post(method, params, true)
} else {
throw err;
}
})
// return result if no error
.then(data => {
// data from 'return request(options)' or 'return post(method, params, true)'
return data.result
});
}
Use
getSomeData.then(data => { /* do something with data */});
I'd suggest that for complex logic at least you use the async/await syntax.
Of course .then() etc is perfectly valid, however you will find the nesting of callbacks awkward to deal with.
My rule (like a lot of things in programming) is use context to make your decision. .then() works nicely when you're dealing with a limited number of promises. This starts to get awkward when you're dealing with more complex logic.
Using async / await for more involved logic allows you to structure your code more like synchronous code, so it's more intuitive and readable.
An example of two approaches is shown below (with the same essential goal). The async / await version is the more readable I believe.
Async / await also makes looping over asynchronous tasks easy, you can use a for loop or a for ... of loop with await and the tasks will be performed in sequence.
function asyncOperation(result) {
return new Promise(resolve => setTimeout(resolve, 1000, result));
}
async function testAsyncOperationsAwait() {
const result1 = await asyncOperation("Some result 1");
console.log("testAsyncOperationsAwait: Result1:", result1);
const result2 = await asyncOperation("Some result 2");
console.log("testAsyncOperationsAwait: Result2:", result2);
const result3 = await asyncOperation("Some result 3");
console.log("testAsyncOperationsAwait: Result3:", result3);
}
function testAsyncOperationsThen() {
return asyncOperation("testAsyncOperationsThen: Some result 1").then(result1 => {
console.log("testAsyncOperationsThen: Result1:", result1);
return asyncOperation("testAsyncOperationsThen: Some result 2").then(result2 => {
console.log("testAsyncOperationsThen: Result2:", result2);
return asyncOperation("testAsyncOperationsThen: Some result 3").then(result3 => {
console.log("testAsyncOperationsThen: Result3:", result3);
})
})
})
}
async function test() {
await testAsyncOperationsThen();
await testAsyncOperationsAwait();
}
test();
... But is it really a good way and isn't there a better way ?
No it's not a good idea because it hurts code readability.
You're mixing 2 interchangeable concepts, Promise.then/catch and async/await. Mixing them together creates readability overhead in the form of mental context switching.
Anyone reading your code, including you, would need to continuously switch between thinking in terms of asynchronous flows(.then/.catch) vs synchronous flows (async/await).
Use one or the other, preferably the latter since it's more readable, mostly because of it's synchronous flow.
Although I don't agree with how you're handling logins, here's how I would rewrite your code to use async/await and try...catch for exception handling:
function getSomeData() {
return post('mySpecialMethod')
}
async function login() {
const params = { } // some params
await post('myLoginMethod', params)
return { success: true }
}
const loginError = [1, 101]
async function post(method, params, isRetry) {
const options = {} // some options
try {
const data = await request(options)
return data.result
} catch (ex) {
const err = ex.error.error || ex.error
if (err) throw err
if (loginError.includes(err.code) && !isRetry) {
await login()
return post(method, params, true)
}
throw err
}
}
I obviously cannot/didn't test the above.
Also worth exploring the libraries which provides retry functionalities.
something like https://www.npmjs.com/package/async-retry
Generally this is not a big problem. You can chain/encapsulate async calls like this.
When it comes to logic it depends on your needs. I think the login state of a user should be checked before calling any API methods that require authentication.
I have a series of API calls I need to make from a 'user profile' page on my app. I need to prioritize or order the calls when I land on the component.
I have tried using async-await on the componentDidMount lifecycle method but when the first call fails, the rest do not get called.
...
async componentDidMount() {
await user.getGameStats();
await user.getFriendsList();
await user.getPlayStyle();
}
...
Despite ordering the calls, I would like them to still execute regardless of whether the preceding calls failed.
You need to account for rejected promises. If you don't catch the error it will stop execution. Just add a catch() block to each function that may fail.
function a(){
return new Promise((r,f)=>{
console.log('a');
r();
});
}
function b(){
return new Promise((r,f)=>{
console.log('b');
f(); // rejecting this promise
});
}
function c(){
return new Promise((r,f)=>{
console.log('c');
r();
});
}
async function d(){
throw new Error('Something broke');
}
(async ()=>{
await a().catch(()=>console.log('caught a'));
await b().catch(()=>console.log('caught b'));
await c().catch(()=>console.log('caught c'));
await d().catch(()=>console.log('caught d'));
})();
It's a dirty solution, but you can do something like this:
user.getGameStats().then({res => {
callGetFriendsList();
}).catch({err =>
callGetFriendsList();
});
function callGetFriendsList() {
user.getFriendsList().then(res => {
user.getPlayStyle();
}).catch(err => {
user.getPlayStyle();
});
}
The ideal and good way would be to call all of them at the same time asynchronously if they are not dependent on the response of the previous request.
Just add an empty catch at the end of each API call as following
async componentDidMount() {
await user.getGameStats().catch(err=>{});
await user.getFriendsList().catch(err=>{});
await user.getPlayStyle().catch(err=>{});
}
I'd catch to null:
const nullOnErr = promise => promise.catch(() => null);
async componentDidMount() {
const [gameStats, friendsList, playStyle] = await Promise.all([
nullOnErr(user.getGameStats()),
nullOnErr(user.getFriendsList()),
nullOnErr(user.getPlayStyle())
]);
//...
}
I also used Promise.all to run the calls in parallel as there seems to be no dependency between the calls.
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);
}
})();