Adds large scenery picture to show article is legit
Disclaimer: This article will not be going over how the async-await works, so some previous knowledge on promises and general async-await is beneficial! However here is an article if you would like to learn about them: _https://medium.com/@bengarrison/javascript-es8-introducing-async-await-functions-7a471ec7de8a
Recently in a large codebase far far away, my team and I were looking into implementing async-await (as we were just using stock standard promises). One of the problems we had come across was catching errors on multiple levels, gracefully (error bubbling).
So first let us get an understanding of what we are trying to improve over with just good old promises: https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html?utm_source=tool.lu
This Will explain the issue with very deeply nested promises and why async-await is great!
Looking into various other implementations of error handling with async-await the common practice seemed to be using a Try Catch (This approach seemed to be a little heavy), basically we just could not find a solid implementation on how to handle error in an async-await call. After all we are just dealing with promises at the end of the day.
So we started to investigate how to bubble a catch block error up the await chain. Lets take a simple example:
async function asyncCall() {
var result = await delayedCall();
console.log(result); // undefined
}
function delayedCall() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('I have failed!');
}, 1000)
})
.catch(error => {
console.error('Catch One: ', error); // Catch One: I have failed!
});
}
asyncCall();
The above promise will always reject, as a result the catch block would be hit and we would get Catch One: I have failed! as expected. Now lets see what happens when we just return the catch error:
async function asyncCall() {
var result = await delayedCall();
console.log(result); // I have failed!
}
function delayedCall() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('I have failed!');
}, 1000)
})
.catch(error => {
console.error('Catch One: ', error); // Catch One: I have failed!
return error;
});
}
asyncCall();
Hmmm okay this is expected, however we would now be unsure if the result is based off an error or is an actual result. We would then, possibly, have to do some validation and it could really just get messy. Lets see what happens if we add a catch to this await:
async function asyncCall() {
var result = await someAsyncCall()
.catch(error => {
console.error('Catch Two: ', error); // Never hit...
});
console.log(result); // I have failed!
}
function someAsyncCall() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('I have failed!');
}, 1000)
})
.catch(error => {
console.error('Catch One: ', error); // Catch One: I have failed!
return error;
});
}
asyncCall();
As expected returning an error does not bubble it up to the next catch block. However what if we had to use a straight Promise.reject
inside the first catch block:
async function asyncCall() {
const result = await someAsyncCall()
.catch(error => {
console.error('Catch Two: ', error); // Catch Two: I have failed!
});
console.log(result); // undefined
}
function someAsyncCall() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('I have failed!');
}, 1000)
})
.catch(error => {
console.error('Catch One: ', error); // Catch One: I have failed!
return Promise.reject(error);
});
}
asyncCall();
So now we get something a little different. We are returning a promise rejection. Meaning that our next catch block will get hit, and our result
would be undefined as we technically never assigned it a value. This gives us a few benefits:
Now you may ask why would I even need to bubble an error? Why should I be taking this approach? Surely I just want to know a specific error at a specific level and fail it right there, no questions asked?
The reason for this type of error catch handling is that it creates a mechanism for the developer to create UI level errors, and handle error degradation on differently levels gracefully. As an example, lets say at the first catch block it failed for a specific reason, you would then possibly show an error to the user and this could possibly stop them from moving further in your application. However lets say at level two, the types of issues you are expecting to catch are not application breaking, the user can simply retry and do a do over. Or even in worst case scenario you manage to catch a programmer error, rather than expected and be able to handle it that way (restarting the system and letting the user know something went wrong on your side).
The types of error handling will differ at these levels, and whats great is that because of this you are then able to handle your errors how you want. It may not even be a UI worthy error, but it just gives you that flexibility.
There are many ways to solve a problem. This one was most suitable for our situation, as we needed different logs on different levels, a try catch or error object handling could be an option, however this seemed the most elegant and straight forward approach.
Let me know of anything I may have missed. And hopefully this approach could be of benefit to you or your team.
Recommended Courses:
☞ Master the MEAN Stack - Learn By Example
☞ ChatBots: Messenger ChatBot with API.AI and Node.JS
☞ Build a Real Time web app in node.js , Angular.js, mongoDB
☞ Getting Started with Node.js - Full Tutorial
☞ How To Create A Password Protected File Sharing Site With Node.js, MongoDB, and Express
☞ From Zero to Forex Trading Bot Hero with Node.js and Typescript
☞ Angular and Nodejs Integration Tutorial