Is logic-heavy Promise chaining "callback hell"? - javascript

I launch my first promise and wait for it (this is the very high-level process of my system, it's where I chain everything together):
install_demo_component.then(), then, I need to handle the response from that:
install_demo_component.then(response => {
if(response.failed) {
undo_changes_we_did_from_install.then(response => {
if(response.failed) {
completely_uninstall_demo_component.then(response => {
if(response.failed) {
throw BIG_ERROR;
}
}).catch(error => {
});
}
}).catch(error => {
});
}
}).catch(error => {
});
But I can't avoid it. Nor can I make it prettier, me thinks. The way these promises are structured to wait for each other or start a new promise chain if response.failed arises is essential to how my system works, it's related to the business logic.
I think a lot of people would see this as callback hell and although I think callback hell in essence is something else, I agree it doesn't look nice.
If it really is callback hell, what's the alternative?

Original Answer
That isn't really promise chaining. To chain promises, you need to return a promise from the .then() method. I would rewrite your code like this:
install_demo_component.then(response => {
if(response.failed) return undo_changes_we_did_from_install
return Promise.reject();
}).then(response => {
if(response.failed) return completely_uninstall_demo_component
return Promise.reject();
}).then(response => {
if(response.failed) throw BIG_ERROR;
return Promise.reject();
}).catch(error => {
//You only need one catch when promise chaining.
});
returning Promise.reject(); will break out of the promise chain and jump to .catch()
Click here for more info on promise chaining.
Error handling with promises
My original response was how to convert your code to a promise chain without changing it too much. I believe that's the best way to help people understand what was changed. However, I do have a suggestion on how you can use promises to their fullest.
Instead of checking for response.failed, you can have your asynchronous functions such as undo_changes_we_did_from_install reject on failure. This will remove the need for all of the conditional statements.
//The function below will either resolve() or reject()
install_demo_component.catch(response => { //Notice only .catch() is used.
return undo_changes_we_did_from_install
}).catch(response => {
return completely_uninstall_demo_component
}).catch(response => {
throw BIG_ERROR;
};
You see .catch() is already invoked conditionally, the condition is if the previous promise rejected instead of resolved. And you see that the .catch() method can also be chained.
For a short article on this go here.

To avoid callback hell, all you have to do is run those install test inside of an async function. This can be done inside of an IIFE:
// returns a Promise
function install_demo_component() {
return new Promise((resolve, reject) => {
resolve({
failed: false,
func: 'install_demo_component'
});
});
}
// returns a Promise
function undo_changes_we_did_from_install() {
return new Promise((resolve, reject) => {
resolve({
failed: true,
func: 'undo_changes_we_did_from_install'
});
});
}
// returns a Promise
function completely_uninstall_demo_component() {
return new Promise((resolve, reject) => {
resolve({
failed: true,
func: 'completely_uninstall_demo_component'
});
});
}
// run install tests inside of async IIFE (Immediately Invoked Function Expression)
(async function() {
// try/catch is used instead of the .catch() method when using async/await
try {
// using the new `...` from ES6 inside of an obj litteral, properties can be accessed from the function
if({ ...await install_demo_component() }.failed) {
console.log('install failed');
} else if({ ...await undo_changes_we_did_from_install()}.failed ) {
console.log('undo install changes failed');
} else if({ ...await completely_uninstall_demo_component() }.failed) {
console.log('completely uninstall demo component failed!!!!, What do we do?!!!!!');
}
} catch(err) {
console.log(err);
}
}());
EDIT
Made the if/else logic make more sense based off of the order of actions needing to take place:
// run install tests inside of async IIFE (Immediately Invoked Function Expression)
(async function() {
// try/catch is used instead of the .catch() method when using async/await
try {
// using the new `...` from ES6 inside of an obj litteral, properties can be accessed from the function
if(!{ ...await install_demo_component() }.failed) {
console.log('install succeed');
} else if(!{ ...await undo_changes_we_did_from_install() }.failed) {
console.log('undo install changes succeed');
} else if(!{ ...await completely_uninstall_demo_component() }.failed) {
console.log('completely uninstall demo component succeed');
} else {
console.log('Everything failed and now this sucker is gaana blow');
}
} catch(err) {
console.log(err);
}

async await does wonders to get rid of all the trailing });
async function ur_func() {
let response = await install_demo_component;
if (response.failed) {
let undo_response = await undo_changes_we_did_from_install
if (undo_response.failed) {
let uninstall_response = completely_uninstall_demo_component;
if (uninstall_response) {
throw BIG_ERROR;
}
}
}
}
ur_func();
You could also use early returns to get rid of the indentation, but that's more a matter of preference.

Related

Should I avoid try catch in every single async/await on Node js?

This is a design question that came up to me while unit testing.
Let's dive into the example:
Imagine this:
async function foo() {
try {
return apiCall()
}
catch (e) {
throw new CustomError(e);
}
}
async function bar() {
return foo()
}
async function main() {
try {
await bar()
}catch(e) {
console.error(e)
}
}
main()
What do we see here? That the only function that hasn't got a try-catch block is bar.
But if foo fails, it should get catched by the main catch.
While unittesting this like
describe('testing bar', () => {
it('foo should throw', () => {
foo.mockImplementantion(() => { throw new CustomError('error')});
bar()
.then((result) => console.log(result))
.catch((err) => { exepect(err).toBeInstanceOf(CustomError)}) // this is what we are testing
})
})
The output we see is that an Unhandled promise rejection is logged in the console.
So, my question is... even if I know that the main() will catch the error, should I use try-catch block inside all async functions?
try..catch may be necessary if a function is able to recover from an error, do a side effect like logging, or re-throw a more meaningful error.
If CustomError is more preferable than an error that apiCall can throw then try..catch necessary, otherwise it doesn't. Also the problem with foo is that it handles only synchronous errors. In order to handle rejected promises, it should be return await apiCall(), this is a known pitfall of async.
Uncaught rejections are unwanted, they currently result in UnhandledPromiseRejectionWarning and are expected to crash Node in future versions. It's preferable to handle an error in a meaningful way at top level, so main needs to catch the error. This can be delegated to process uncaughtRejection event handler but it may be beneficial for it to stay extra level of error handling that should be never reached.
The output we see is that an Unhandled promise rejection is logged in the console.
This shouldn't happen. A rejection needs to be handled by the test. One possible point of failure is explained above, foo can return original error from apiCall instead of CustomError in case it wasn't correctly mocked, this will fail the expectation and result in unhandled rejection in catch(). Another point of failure is that the test has unchained promise because it wasn't returned, the test always passes.
Asynchronous test that uses promises should always return a promise. This can be improved by using async..await. foo is async, it's expected to always return a promise:
it('foo should throw', async () => {
foo.mockImplementantion(() => { return Promise.reject(new CustomError('error')) });
await expect(bar()).rejects.toThrow(CustomError);
})
Now even if foo mock fails (foo mock won't affect bar if they are defined in the same module as shown) and bar rejects with something that is not CustomError, this will be asserted.
No. You don't need to use try/catch in every async/await. You only need to do it at the top level. In this case your main function which you are already doing.
Weather you should is a matter of opinion. The go language designers feel strongly enough about this that is has become the standard in go to always handle errors at each function call. But this is not the norm in javascript or most other languages.
Unhandled promise rejection
Your unhandled promise rejection is thrown by your it() function because you are not telling it to wait for the promise to complete.
I assume you are using something like mocha for the unit test (other frameworks may work differently). In mocha there are two ways to handle asynchronous tests:
Call the done callback - the it() function will always be called with a done callback. It is up to you weather you want to use it or like in your posted code to not use it:
describe('testing bar', () => {
it('foo should throw', (done) => {
foo.mockImplementantion(() => { throw new CustomError('error')});
bar()
.then((result) => {
console.log(result);
done(); // ------------- THIS IS YOUR ACTUAL BUG
})
.catch((err) => {
exepect(err).toBeInstanceOf(CustomError);
done(); // ------------- THIS IS YOUR ACTUAL BUG
})
})
})
Return a Promise. If you return a promise to the it() function mocha will be aware that your code is asynchronous and wait for completion:
describe('testing bar', () => {
it('foo should throw', (done) => {
foo.mockImplementantion(() => { throw new CustomError('error')});
return bar() // <----------- THIS WOULD ALSO FIX IT
.then((result) => {
console.log(result);
})
.catch((err) => {
exepect(err).toBeInstanceOf(CustomError);
})
})
})
In short, there is nothing wrong with your code. But you have a bug in your unit test.
As #Bergi told me I will post some solutions right here
I wrap the function in a try catch block
1.
async function bar() {
try{
return foo()
} catch (e) {
throw e
}
}
Rewrite the test
describe('testing bar', () => {
it('foo should throw', (done) => {
foo.mockImplementantion(() => { throw new CustomError('error')});
bar()
.then((result) => { throw result }) // this is because we are expecting an error, so if the promise resolves it's actually a bad sign.
.catch((err) => {
exepect(err).toBeInstanceOf(CustomError)}) // this is what we are testing
done();
})
})
Use return in the test case
describe('testing bar', () => {
it('foo should throw', () => {
foo.mockImplementantion(() => { throw new CustomError('error')});
return bar()
.then((result) => { throw result })
.catch((err) => { exepect(err).toBeInstanceOf(CustomError)}) // this is what we are testing
})
})

Different behaviour between 'returning a promise' and 'resolving with promise' inside a `new`-generated promise

It's not much I discovered Javascript Promise.
However I found out a behaviour I couldn't understand concerning the nesting of (new or returned) Promise inside Promises
That's the background (extPromiseX is a third-party function returning Promise):
Case 1:
function myAction(param) {
return extPromise1(sql).then((result) => {
[...]
return extPromise2(sql2).then((result) => {
[...]
return extPromise3(sql3);
})
});
}
// Main Process
myAction(...)
.then(() => {
console.log('Process Terminated');
}).catch((error) => {
console.error('Exit with error', error);
});
Now, as expected, I got from the console, in
1) extPromise1 completed
2) extPromise2 completed
3) extPromise3 completed
4) Process Terminated
Case 2:
function myAction(param) {
return new Promise(function() {
if (itsAllOkWithInputs) {
// Some sync code here
return extPromise1(sql).then((result) => {
[...]
return extPromise2(sql2).then((result) => {
[...]
return extPromise3(sql3);
})
})
} else {
throw 'Something went wrong';
}
});
}
// Main process
myAction(...)
.then(() => {
console.log('Process Terminated');
}).catch((error) => {
console.error('3) --> Exit with error', error);
})
In this second case extPromises are executed but the very first Promise remains pending (confirmed by debug). So the console shows:
1) extPromise1 completed
2) extPromise2 completed
3) extPromise3 completed
I empirically realized that I had to change myAction function as follow for the code to work:
function myAction(param) {
return new Promise(function(resolve, reject) {
if (itsAllOkWithInputs) {
// Some sync code here
let ep = extPromise1(sql).then(...);
resolve(ep);
} else {
throw 'Something went wrong';
}
});
}
My question:
I thought returning a promise inside another parental one would make
the parent resolving with the results of the child. This is the case
inside the then code block applied to the external promises. Why
this is not valid for the new Promise case?
Because .then is meant to chain promises. You can return a Promise from inside the then callback, and then itself will return a new Promise.
The Promise constructor is supposed to construct a Promise from an underlying callback. If you return a Promise from inside a Promise constructor, you are doing something wrong conceptually. And that's why it does not work.
function myAction(param) {
if (itsAllOkWithInputs) {
return extPromise1(sql).then(...);
} else {
return Promise.reject('Something went wrong');
}
}
// OR
async function myAction(param) {
if (itsAllOkWithInputs) {
await extPromise1(sql);
} else {
throw 'Something went wrong';
}
}
new Promise(function() {
You are not using neither resolve nor reject arguments (you actually even haven't declared them), so the promise will never get resolved or rejected. All other stuff is irrelevant.
If your function returns promise, you have to simply call it and return a result. If you don't know whether the function return promise or just a value, you can wrap it's call into Promise.resolve, but not in new Promise.

How can I order a series of api calls from the frontend?

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.

Improved way to deal with callbacks inside promises

I have the following code that uses callbacks inside promises:
const clue = 'someValue';
const myFunction = (someParam, callback) => {
someAsyncOperation(someParam) // this function returns an array
.then((array) => {
if (array.includes(clue)){
callback(null, array); // Callback with 'Success'
}
else{
callback(`The array does not includes: ${clue}`); // Callback with Error
}
})
.catch((err) => {
// handle error
callback(`Some error inside the promise chain: ${err}`) // Callback with Error
})
}
and call it like this:
myFunction (someParam, (error, response) => {
if(error) {
console.log(error);
}
else {
// do something with the 'response'
}
})
Reading some documentation, I found that there is some improved way to do this:
const myFunction = (someParam, callback) => {
someAsyncOperation(someParam) // this function returns an array
.then((array) => {
if (array.includes(clue)){
callback(array);
}
else{
callback(`The array does not includes: ${clue}`);
}
}, (e) => {
callback(`Some error happened inside the promise chain: ${e}`);
})
.catch((err) => {
// handle error
callback(`Some error happened with callbacks: ${err}`)
})
}
My question:
In the sense of performance or best practices, it's okay to call the 'callback' function inside the promise as the two ways show or I'm doing something wrong, I mean some promise anti-pattern way ?
This seems really backwards and takes away from the benefits of promises managing errors and passing them down the chain
Return the asynchronous promise from the function and don't interrupt it with callbacks. Then add a catch at the end of the chain
const myFunction = (someParam) => {
// return the promise
return someAsyncOperation(someParam) // this function returns an array
.then((array) => {
return array.includes(clue) ? array : [];
});
}
myFunction(someParam).then(res=>{
if(res.length){
// do something with array
}else{
// no results
}
}).catch(err=>console.log('Something went wrong in chain above this'))
Do not use callbacks from inside of promises, that is an anti-pattern. Once you already have promises, just use them. Don't "unpromisify" to turn them into callbacks - that's moving backwards in code structure. Instead, just return the promise and you can then use .then() handlers to set what you want the resolved value to be or throw an error to set what you want the rejected reason to be:
const clue = 'someValue';
const myFunction = (someParam) => {
return someAsyncOperation(someParam).then(array => {
if (!array.includes(clue)){
// reject promise
throw new Error(`The array does not include: ${clue}`);
}
return array;
});
}
Then, the caller would just do this:
myFunction(someData).then(array => {
// success
console.log(array);
}).catch(err => {
// handle error here which could be either your custom error
// or an error from someAsyncOperation()
console.log(err);
});
This gives you the advantage that the caller can use all the power of promises to synchronize this async operation with any others, to easily propagate errors all to one error handler, to use await with it, etc...

how to reject promise inside a then

I want to be able to reject the entire promise chain if anyone of the promise fails in a clean way. I want to "catch" this rejection and send an error notification. I have implemented it the following code:
let reportMetaData = api.ajaxGet(api.buildV3EnterpriseUrl('reports' + '/' + params.report_id))
.catch(error => {
if (error.status === constants.HTTP_STATUS.GATEWAY_TIMEOUT) {
this.notify.error(this.translate('reports.report_timedout'), this.translate('reports.report_timedout_desc'));
} else {
this.send('error', error);
}
});
let aggregateData = reportMetaData.then(success => {
try {
return api.xmlRequest('GET', success.aggregationUrls.elements[0].url);
} catch (error) {
return Promise.reject();
}
}).then(rawData => {
try {
return JSON.parse('{' + rawData + '}');
} catch (error) {
return Promise.reject();
}
}, error => Promise.reject(error));
let aggregateReport = aggregateData.then(data => {
if (!data || !data.report) {
return Promise.reject();
}
return data.report;
}).catch(error =>{
this.notify.error(this.translate('reports.report_timedout'), error);
});
As you can see, it is super messy and complicated. Is there a way I can simplify this? I want the simplest way to reject the entire promise to fail if anyone promise fails. How do I do that from inside the then function? Also, it seems like the thrown error is bubbling up all the way to chrome console as uncaught error. Why does it bubble up even though I caught it?
You need to use Promise.all() and provide the array of promises as input parameter.
If one of those promises will fail, all the promises will not be resolved.
Here the doc:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
And here a post on SO where you can read about them:
When to use promise.all()?
Try aggregating everything under Promise.all(iterable).
More here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
If this is not quite what you wanted, take a look at Bluebird - fully featured promise library. Here
UPDATE: If you want to reject the entire promise if any of the promises inside the function fails, try implementing:
throw validationError;
Hope it works.
You could use async functions to clean things up a bit. I think you could replace your code with the following.
async function processDataAndReport() {
try {
const data = await api.ajaxGet(api.buildV3EnterpriseUrl('reports' + '/' + params.report_id));
const rawData = await api.xmlRequest('GET', data.aggregationUrls.elements[0].url);
const { report } = JSON.parse(`{${rawData}}`);
} catch(e) {
// send notification
}
}

Categories