Let's continue our previous article and dig a little deeper about bugs and errors.
Cleaning Up After Exceptions
Exceptions have side effects and they always happen even when the regular control flow is performed. The effect of an exception is another kind of control flow.
Let's see an example of bad code:
const accounts = {
a: 100,
b: 0,
c: 20
};
function getAccount(){
let accountName = prompt("Enter an account name");
if(!accounts.hasOwnProperty(accountName))
{
throw new Error(`No such account: ${accountName}`);
}
return accountName;
}
function transfer(from, amount){
if(accounts[from] < amount) return;
accounts[from] -= amount;
accounts[getAccount()] += amount;
}
The transfer function will transfer the money from a given account to another. If the account name is invalid, getAccount() will throw an error.
But transfer will first remove the money from the account. Somehow if it gets broken then the amount will get disappear.
One way to address this is to compute new values rather than changing existing data. But this isn't always practical. Instead, we can use another approach of using finally block with try and catch block.
A finally block always runs no matter what whether the exception happens or not.
So, we can change the above code like this ๐
function transfer(from, amount){
if(accounts[from] < amount) return;
let progress = 0;
try{
accounts[from] -= amount;
progress = 1;
accounts[getAccount()] += amount;
progress = 2;
}
finally{
if(progress == 1){
accounts[from] += amount;
}
}
}
This will track the progress and if when leaving, it notices some damage, it will repair.
Even if the try block throws an exception, finally block will not interfere with it.
Selective Catching
JavaScript doesn't provide selective catching directly: either you catch them all or don't catch any. This makes it tempting to assume that the exception you get is the one you were thinking about.
But it might not always be the case.
for(; ;){
try{
let dir = promtDirection("where?");
console.log("You chose: "+dir);
break;
}
catch(e){
console.log("Not a valid direction.Try again.");
}
}
This infinite loop will break only when the correct direction will be put. But we misspelled the function which will result in "undefined variable" error.
Now, the catch block will ignore its exception value(e) and will assume that it knows what exception is. So, we want to catch a specific kind of exception.
One way to do it is to compare message property against the error message. But is ineffective as someone changes the message then the code will stop working.
So, another way of doing it is to define a new type of error and use instanceof to identify it.
class InputError extends Error{}
function promptDirection(question){
let result = prompt(question);
if(result.toLowerCase() == "left") return "L";
if(result.toLowerCase() == "right") return "R";
throw new InputError("Invalid Direction: "+result);
}
The new error class extends Error. It doesn't define its own constructor means it will inherit the Error constructor. InputError objects will behave like the Error objects except they have different class.
for(; ;){
try{
let dir = promptDirection("where?");
console.log("You chose: ", dir);
break;
}
catch(e){
if(e instanceof InputError)
{
console.log("Not a valid direction. Try again.");
}
else{
throw e;
}
}
}
This will catch only instances of InputError. If you reintroduce the typo then it will properly report the "undefined variable" error.
Assertions
They are the checks inside the program to verify something is in the way it is supposed to be. They are generally used to find programmer mistakes. For example, the first element should never be called on empty arrays.
function firstElement(array){
if(array.length == 0)
{
throw new Error("array is empty");
}
return array[0];
}
Now instead of silently returning undefined, this will blow up your program.
But writing assertions make the code noisy. So, we should use them only for the mistakes that are easy to make.
And with this, we came to an end of the topic "Bugs and Errors".
Hope you like it. โ