- Check-first error handling
- The
try..catch
construct - The full form of
try..catch..finally
- The
throw
statement - 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
,ReferenceError
etc. 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 statements
are executed. If no errors occur, then thecatch
section is ignored. - In case of an error, the
exception
variable is assigned to the error object andcatch statements
are executed. - In both cases, after either successful
try
orcatch
, thefinally
code 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..catch
way is usually cleaner and more reliable. It catches all errors. - There exist actions which you can’t check. So the
try..catch
is only the way to go. For example, testing some browser’s features is done by executing the code and watching for exceptions. - The
try..catch
construct 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
try
block is complex. It may throw errors, some of them we know how to process, likeValidationError
. But other kinds of errors are possible. - In the
catch
section 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..catch
block 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
.