- Check-first error handling
- The
try..catchconstruct - The full form of
try..catch..finally - The
throwstatement - Exception analysis and rethrow
- Summary
Understanding exception is important to object-oriented programming in general and JavaScript in particular.
Exceptions is a special, very powerful way to deal with errors.
Check-first error handling
Let’s take a bad code as an example. Like this:
nonexistant()
In the example above, a non-existing variable is accessed. What should a program do in this case?
The simple and very old concept is that the program dies. Let’s say we want to evade that sorrowful happening.
So, what to do in case when the variable may be undefined?
The simple way is to check it. Something like that:
if (window.func) {
func()
}
There still may be an error if window.func is not a function. So we’ll need to check for it as well:
if (typeof(func) == 'function') {
func()
}
In the code above, typeof ensures that the variable exists and it is actually a function.
Hopefully we performed all necessary checks to ensure that running func is safe. But what if it isn’t? What if the func body has errors? Again, we want to handle the error, not let the script just die.
And here the try..catch construct kicks in.
The try..catch construct
The try..catch approaches the error handling from another side. Instead of “check if all ok, then do” approach, we try then catch errors.
A completely different way to handle errors which replaces the checking code.
The function example would look like:
try {
func()
} catch(e) {
alert(e)
}
If an error occurs inside the try block, the control is passed to the catch(e) section.
It’s argument e is assigned to a special exception object which contains the information about what happened.
The variable e contains an instance of Error object (or it’s inheritant like TypeError, ReferenceError etc).
The error properties are little bit different between browsers, see Error in MDN and Error in MSDN for details.
But there are always basic attributes:
name- The error type, for browser-generated errors it matches error constructor function, like
TypeError,ReferenceErroretc. message- The text message which tells more about the error.
Now let’s go further and add other statements into try section. In the example below, both name and message are printed.
try {
var a = 5
var res = func(a)
if (res > 0) doA()
else doB()
} catch(e) {
alert("name:" + e.name + "\nmessage:" + e.message)
}
Do you know one cool thing about the try..catch?
There are errors which can only be caught by try..catch, because you can’t detect a possible fail until you try.
That makes the try..catch construct extremely valuable and important.
Obtaining the stack
Browsers Firefox, Chrome, Opera provide additional stack property which allows to see the nested calls which led to the exception. Check it on the example below.
function f(a) {
g(a+1)
}
function g(a) {
notexists;
}
try { f(1) } catch(e) { alert(e.stack) }
Unfortunately, IE does not have this property even in IE9.
The full form of try..catch..finally
The full form of try..catch construct consists of three parts:
try {
.. try statemenets ..
} catch(exception) {
.. catch statements ..
} finally {
.. finally statements ..
}
Works like this:
- The
try statementsare executed. If no errors occur, then thecatchsection is ignored. - In case of an error, the
exceptionvariable is assigned to the error object andcatch statementsare executed. - In both cases, after either successful
tryorcatch, thefinallycode is executed.
The finally clause is used to perform actions which should be done in any way, like removing loading indicator in both cases: success or error. It is possible to omit catch if finally is provided:
// rarely used, but valid
try {
..
} finally {
..
}
try..catch..finally and return
The finally works in any case of leaving the try block.
In the following example, the return occurs from inside try, but finally still intercepts it and executes before the control is passed to the calling code.
function inc(a) {
try {
return a+1
} catch(e) {
// ..
} finally {
alert('done')
}
}
alert( inc(1) )
The throw statement
Most errors can be split into two kinds:
- Programmatic errors
- The errors which occur because a developer did something wrong. An often example is a mistype.
- Execution flow errors
- An error which is a normal part of execution. A usual example is form validation. If the user entered something wrong, then it is normal to process the error and ask him to repeat.
It may be beneficial to use the try..catch with execution flow errors. To do it, we should be able to raise our own errors, which is done by throw.
The syntax is: throw e, where e is literally anything. No matter what you throw, it will be caught by the catch… Or make the program die if throw is done out of try section.
The example below demonstrates the idea of how throw works.
try {
throw 5
} catch(e) {
alert("Caught: "+e)
}
A validator example
For example, let’s write an age validator. It takes a variable and check it for valid age:
function validateAge(age) { // age is a text to check
if (age === '') return // no age is valid
age = +age
if (isNaN(age)) {
throw { name: 'BadAge', message: 'Invalid age' }
}
if (age < 5 || age > 150) {
throw { name: 'BadAge', message: 'Age out of range' }
}
}
try {
var age = prompt("Enter your age please?")
validateAge(age)
alert("The age is accepted!")
} catch(e) {
alert("Error: "+e.message)
}
Usually, it is better to inherit error objects from Error and support a manageable error hierarchy. So in the example above, there should be throw new BadAgeError("Invalid age").
By the way, not how the validator usage pattern gets changed.
Changes in the usage pattern
For example, we need to validate if a value is provided and that it’s a valid age. To do so, we implement validateAge and validateRequired.
We validate until first error.
The error-checking way:
Without exceptions, the validator could return either true or false. But that’s not enough, we need to know the error. So let it return the error object in case of error and undefined if all ok.
The usage pattern would be:
var value = input.value
// VALIDATE
var error = validateRequired(value)
if (!error) {
error = validateAge(value)
}
if (!error) {
/* another validator... */
}
// FINISHED VALIDATING
if (error) {
/* process error */
} else {
/* success */
}
The try..catch way:
The validator returns nothing in this case. Actually, it just checks the value and throws an error if finds it.
var value = input.value
try {
validateRequired(value)
validateAge(value)
// other validators in a row
/* success */
} catch(e) {
/* process error */
}
It is important and code-saving that we put many actions into the try block, not check them one-by-one. If all is fine, then all is fine. If something goes wrong, we’ll see what is it in the catch section.
Comparison
Here are advantages and disadvantages of using try..catch for error handling.
- The
try..catchway is usually cleaner and more reliable. It catches all errors. - There exist actions which you can’t check. So the
try..catchis only the way to go. For example, testing some browser’s features is done by executing the code and watching for exceptions. - The
try..catchconstruct itself takes several lines. The obvious overhead for simple stuff.
Exception analysis and rethrow
Sometimes, the code may produce different types of errors. In this case, the if is used to choose the correct action.
Here is a pseudocode, assuming all excepition object are instances of proper-named error objects:
try {
// 1. do smth
} catch(e) {
if (e instanceof ValidationError) {
// 2.1 process e
} else if (e instanceof PermissionError) {
// 2.2 process e
} else {
// 3. we don't know how to deal with e
throw e
}
}
- The code in the
tryblock is complex. It may throw errors, some of them we know how to process, likeValidationError. But other kinds of errors are possible. - In the
catchsection we analyze the exception and process it if we are able to. - Otherwise, the exception is rethrown. It is assumed that there is an outer
try..catchblock which knows how to deal with the error.
It is extremely important that an exception must be processed or rethrown, not left alone, unless you absolutely know what you’re doing.
try {
func()
} catch(e) {
if (e instanceof KnownError) {
// ...
}
}
In the snippet above, other exception types except KnownError are silently ignored.
Well, frankly, the antipattern of leaving exception unprocessed is more from the Java world. But anyway, leaving an exception object is dangerous.
Imagine, there is a mistype in the func in the example above. It will be very hard to debug, because all TypeError and ReferenceError exceptions are cought and ignored.
Summary
The try..catch..finally allows to join several statements in a single code-block try, and split error-handling into the separate catch block.
It allows to handle all errors, both JavaScript-generated and thrown manually.
Technically, JavaScript allows to throw any value, but it is recommended that all your errors inherit the basic Error object and form an hierarchy. In this case, instanceof works well.
For example, you can catch all e instanceof ValidationError including AgeValidationError, RequiredValidationError etc. Your own exceptions may have additional properties like extra or cause.