Given a number of nested promises structured similarly to the example below, is it possible, given a timeout result in delta() to neither return, resolve, nor reject the timeout, and not throw Error(timeout), but somehow exit the JavaScript call stack and reset to a new screen?
Context is React Native app
Nav library is react-native-navigation
Goal is to not throw Error(timeout)
We do not want the error to bubble up through the promise chain
The promise nesting occurs in unique variations in 100s of places in the app
We do not want individually handle a timeout response/error in each spot
Hoping instead to somehow exit the call stack and reset the screen
react-native-navigation has a slew of methods to setRoot, navigateToScreen, etc, but these don't exit call stack
const alpha = async () => {
try return await bravo()
catch (e) throw Error(e)
}
const bravo = async () => {
try return await charlie()
catch (e) throw Error(e)
}
const charlie = async () => {
try return await delta()
catch (e) throw Error(e)
}
const delta = async () => {
try {
const res = await API()
if (res === timeout) // clearCallStackAndResetToNewScreenDoNotThrowError() POSSIBLE?
else return res
} catch (e) {
throw Error(e)
}
}
const one = async () => {
try {
await alpha()
} catch (e) {
uniqueErrorHandling(e)
}
}
const two = async () => {
try {
await alpha()
} catch (e) {
uniqueErrorHandling(e)
}
}
one()
two()
etc ...
Related
I'm facing a problem of exception catching when the context is async.
Here is my minimal reproducible code
const { EventEmitter } = require('events');
const delaySync = function(ttd) {
return new Promise((resolve) => {
setTimeout(() => resolve(0), ttd);
});
};
const myEvent = new EventEmitter();
myEvent.on('something', async () => {
console.log('Event triggered');
// Just delay for 100ms and then throw
await delaySync(100);
throw new Error('Something happened');
});
describe('Events', () => {
it('should catch error', async () => {
await expect(myEvent.emit('something')).rejects.toThrow('Something happened');
});
});
Basically, I have an event emitter. It has a callback attached on event 'something' which is async. There are some async jobs inside and after 100ms delay it throws an error.
Normally I'd catch such errors in jest using await expect(fn).rejects.toThrow(...) however this case is different because expect fn expects a promise and myEvent.emit('something') is regular non-promise context.
In this case, I'm getting an error
● Events › should catch error
expect(received).rejects.toThrow()
Matcher error: received value must be a promise or a function returning a promise
Received has type: boolean
Received has value: true
...
(node:20242) UnhandledPromiseRejectionWarning: Error: Something happened
I've tried it with try-catch block as well but it did not work (not able to catch at all);
try {
myEvent.emit('something');
await delaySync(250);
} catch (e) {
expect(e.message).toBe('Something happened');
}
I've tried
// Not throwing because exception is not happening right away
expect(() => myEvent.emit('something')).toThrow('Something happened');
Also this
expect(async () => {
// Will thow after 100ms
myEvent.emit('something');
await delaySync(250);
}).toThrow('Something happened');
But no luck with any of them. 😕
Any ideas on how can I catch the error in this case? or any workaround?
The Eventemitter is fully sync and there is no workaround for it. What you can do is to use the eventemitter2 module to replace the original Eventemitter class. It is fully compatible with Eventemitter and allows you to use async actions.
var EventEmitter = require('eventemitter2');
const delaySync = function (ttd) {
return new Promise((resolve) => {
setTimeout(() => resolve(0), ttd);
});
};
const myEvent = new EventEmitter();
myEvent.on('something', async () => {
console.log('Event triggered');
// Just delay for 100ms and then throw
await delaySync(100);
throw new Error('Something happened');
// for async emitters pass promisify true.
}, { promisify: true });
describe('Events', () => {
it('should catch error', async () => {
await expect(myEvent.emit('something')).rejects.toThrow('Something happened');
});
}
let me explain what I mean using an example
async function async_function(){
await new Promise(r=>setTimeout(r,3000));
throw 'task completed'
}
async function do_something_meanwhile() {
await new Promise(r => setTimeout(r, 500));
console.log(Math.floor(Math.random()*10));
}
(async ()=>{
try {
async_function(); //this returns an error after a while
while (...)
await do_something_meanwhile();
} catch (err) { console.log('exited with error:',err) }
console.log('moving on');
})();
I'm trying to run an async function and after it is complete immediately terminate the loop,
the best way I could think of (without any time delay) was to send an error
but it gives this error instead of moving on after it's done:
node:internal/process/promises:246
triggerUncaughtException(err, true /* fromPromise */);
^
[UnhandledPromiseRejection: This error originated either by throwing
inside of an async function without a catch block,
or by rejecting a promise which was not handled with
.catch(). The promise rejected with the reason "task
completed".] {
code: 'ERR_UNHANDLED_REJECTION'
}
is there a way around this or a better to achieve the desired effect?
You can handle rejection by setting an error variable that you can check in the loop:
try {
let error;
async_function()
.catch(err => error = err);
while (...) {
if (error) {
throw error;
}
await do_something_meanwhile();
}
} catch (err) {
console.log('exited with error:',err)
}
If you need to proactively tell do_something_meanwhile to terminate as well, you could use an AbortController and pass its signal to do_something_meanwhile.
try {
let error;
const controller = new AbortController();
const { signal } = controller;
async_function()
.catch(err => {
error = err;
controller.abort();
});
while (...) {
if (error) {
throw error;
}
await do_something_meanwhile(signal);
}
} catch (err) {
console.log('exited with error:',err)
}
I think if I were doing that, I might subclass AbortController so I can put the error in it:
class AbortContollerWithError extends AbortController {
abort(error) {
this.error = error;
super.abort();
}
}
then:
try {
const controller = new AbortController();
const { signal } = controller;
async_function()
.catch(err => {
controller.abort(err);
});
while (...) {
if (signal.aborted) {
throw controller.error;
}
await do_something_meanwhile(signal);
}
} catch (err) {
console.log('exited with error:',err)
}
...or something along those lines.
You asked how you'd use the signal in do_something_meanwhile, and suggested in a comment that you're really using a timer in it. That's where the signal's abort event comes in handy, you can use that to settle the promise early:
async function do_something_meanwhile(signal) {
let cancelError = {};
try {
await new Promise((resolve, reject) => {
const timer = setTimeout(resolve, 500);
signal.addEventListener("abort", () => {
clearTimeout(timer);
cancelError = new Error();
reject(cancelError);
});
});
console.log(Math.floor(Math.random() * 10));
} catch (error) {
if (error === cancelError) {
// Probably do nothing
} else {
// Something else went wrong, re-throw
throw error;
}
}
}
Promise.all can run async_function and do_something_meanwhile in parallel mode.
While Promise/A doesn't have a cancel method, you can define a stopFlag, and check it in do_something_meanwhile function and the while loop.
let stopFlag = false
async function async_function() {
await new Promise(r=>setTimeout(r, 3000));
throw 'task completed'
}
async function do_something_meanwhile() {
await new Promise(r => setTimeout(r, 500));
if (!stopFlag) {
console.log(Math.floor(Math.random() * 10));
}
}
(async()=>{
try {
await Promise.all([
async_function().catch((err) => {
stopFlag = true
throw err
}), // this returns an error after a while
(async () => {
while (!stopFlag)
await do_something_meanwhile();
})()
])
} catch (err) {
console.log('exited with error:', err)
}
console.log('moving on');
})();
So I have this user service functions in service.ts which includes database stuffs.
export const service = {
async getAll(): Promise<User[]> {
try {
const result = await query
return result
} catch (e) {
report.error(e)
throw new Error(e)
}
},
...
}
And query.ts file for some reasons, eg: caching, business logic, etc.
export const query = {
async index(): Promise<User[]> {
try {
const result = await service.getAll()
return result
} catch (e) {
report.error(e)
throw new Error(e)
}
},
...
}
And another upper layer for routers and resolvers, because I want to see all routes in one file.
export const resolver = {
Query: {
users: (): Promise<User[]> => query.index(),
},
...
}
Do I need to wrap try...catch in all functions? Or can't I just add .catch at the very top layer like this:
export const resolver = {
Query: {
users: (): Promise<User[]> => query.index().catch(e => e),
},
...
}
Do I need to wrap try...catch in all functions?
No, you don't, not unless you want to log it at every level for some reason. Just handle it at the top level.
In an async function, promise rejections are exceptions (as you know, since you're using try/catch with them), and exceptions propagate through the async call tree until/unless they're caught. Under the covers, async functions return promises and reject those promises when a synchronous exception occurs or when a promise the async function is awaiting rejects.
Here's a simple example:
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function outer() {
// ...
await delay(10);
console.log("before calling inner");
await inner();
console.log("after calling inner (we never get here)");
}
async function inner() {
// ...
await delay(10);
console.log("inside inner");
// Something goes wrong
null.foo();
}
outer()
.catch(e => {
console.log("Caught error: " + e.message, e.stack);
});
Just as a side note: If you do catch an error because you want to do X before the error propagates and you're going to re-throw the error after you do X, best practice is to re-throw the error you caught, rather than creating a new one. So:
} catch (e) {
// ...do X...
throw e; // <== Not `throw new Error(e);`
}
But only do that if you really need to do X when an error occurs. Most of the time, just leave the try/catch off entirely.
Essentially I have an async function containing a try/catch that calls another async function also containing a try catch, and I'm getting a bit confused about how to properly implement what I'm doing. Some "pseudocode" showing my current implementation:
const main = async () => {
try {
const test = await secondFunc();
console.log(test);
} catch(err) {
console.log('Found an error!');
console.log(err);
}
const secondFunc = async () => {
try {
await performSomeRequestExample();
} catch(err) {
if (err.x === 'x') {
doSomething();
} else {
//********
throw err;
//********
}
}
So what I'm trying to do is get the throw(err) (surrounded by the asterisks) to be caught by the catch in main() which will also call the console.log('Found an error!'), but what currently happens is the error is thrown from secondFunc(), the catch in main() is never hit and I get an unhandled promise rejection.
Any guidance on what I'm doing wrong?
My advice is to minimize using try/catch unless absolutely necessary. With async functions (or any functions that return a Promise object) you can usually simplify things by not worrying about try/catch blocks unless you need to do something specific with certain errors. You can also use .catch rather than try/catch blocks to make things easier to read.
For example your code above could be written like this:
const main = async () => {
const test = await secondFunc().catch(err => {
console.log("Found an error from secondFunc!", err);
throw err; // if you want to send it along to main's caller
});
if (test) {
console.log("Test", test);
}
};
const secondFunc = () => {
return performSomeRequestExample().catch(err => {
if (err.x === "x") {
doSomething();
} else {
throw err;
}
});
};
const performSomeRequestExample = () => Promise.reject("bad");
main().then(
() => console.log("worked"),
err => console.log("failed from main", err)
);
In secondFunc we don't need to use async since we can just return the promise coming back from performSomeRequestExample and handle any failures in the .catch.
You should use
const secondFunc = async () => {
performSomeRequestExample().then(res =>{
console.log(res);
})
.catch(err => {
console.log(err);
}
)
Add a return before the await of performSomeRequestExample.
const secondFunc = async () => {
try {
return await performSomeRequestExample();
} catch (err) {
if (err.x === 'x') {
console.log('x');
} else {
throw err;
}
}
}
or you can also use .catch() after the awaited function.
Another solution can be like this
const main = async() => {
try {
const test = await secondFunc();
console.log(test);
} catch(err) {
console.log('Found an error!');
console.log(err);
}
}
const secondFunc = async () => {
//return await performSomeRequestExample(); //for success
return await performSomeRequestExample(2); //for error
}
const performSomeRequestExample = async(abc=1) => {
return new Promise(function(resolve,reject){
if(abc ==1){
setInterval(resolve("yes"),400);
}else{
setInterval(reject("opps"),400);
}
});
}
main();
Test this code at this link:
https://repl.it/repls/JoyfulSomberTelevision
Hi I am write a unit test case of this function. When I run this function from the unit test case then it covers all statements but setInterval complete lines are not covered.
Does anyone know how to cover it in javascript? I am using mocha.
const open = async function (config) {
...... set of lines..
let retryIn = setInterval(async () => {
try {
client = await connect(config);
clearInterval(retryIn);
return client;
} catch (err) {
//error
}
}, 20000);
};
I am simply calling it like this
it("###test", async () => {
open(config);
});
});
First of all, you should never use setInterval in the case where you want to retry a task that has failed. You should use setTimeout instead.
Besides that, you cannot return a value from a classical callback base function like setInterval or setTimeout. So in its current form, the promise returned when calling open will be resolved before any connection is made.
With await/async you can create a clean and simple setup for such a situation:
function wait(seconds) {
return new Promise((resolve, _) => setTimeout(resolve, seconds))
}
async function connect() {
throw new Error('failed')
}
async function open(config) {
let client;
while (client === undefined /* || retries > maxRetries*/ ) {
try {
client = await connect(config);
} catch (err) {
// if connection failed due to an error, wait n seconds and retry
console.log('failed wait 2 seconds for reconnect');
await wait(2000)
// ++retries
}
}
if (!client) {
throw new Error('connection failed due to max number of retries reached.')
}
return client
}
async function main() {
let connection = await open()
}
main().catch(err => console.log(err))
You can further extend this snippet by adding a retry limit. See the comments for a rough idea on how that can be achieved.
To test the above code, you would write:
it("###test", function() {
return open(config);
});
Someone posted an answer about fake timers and then deleted it , The answer was correct so I re-posted again.
You can use sinonjs to create fake timers
Fake timers are synchronous implementations of setTimeout and friends
that Sinon.JS can overwrite the global functions with to allow you to
more easily test code using them
But from your code, it seems you are trying to test async code, in mocha, this can be achieved like this
describe('somthing', () => {
it('the result is 2', async () => {
const x = await add(1, 1)
expect(x).to.equal(4);
});
});
With something closer to your code
async function open() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('done')
}, 1000);
});
};
describe('somthing', () => {
it('###test', async () => {
const x = await open()
chai.expect(x).to.equal("done");
});
});
Just wrap to Promise
const open = async function (config) {
...... set of lines..
return new Promise((resolve, reject) => {
let retryIn = setInterval(async () => {
client = await connect(asConfigParam);
clearInterval(retryIn);
return client;
}, 20000);
return resolve(retryIn);
});
};
it("###test", async () => {
const result = await open(config);
console.log('result', result)
});