Writing programs that work when everything goes as expected is a good start. Making your programs behave properly when encountering unexpected conditions is where it really gets challenging.
The problematic situations that a program can encounter fall into two categories: Programmer mistakes and genuine problems. If someone forgets to pass a required argument to a function, that is an example of the first kind of problem. On the other hand, if a program asks the user to enter a name and it gets back an empty string, that is something the programmer can not prevent.
In general, one deals with programmer errors by finding and fixing them, and with genuine errors by having the code check for them and perform some suitable action to remedy them (for example, asking for the name again), or at least fail in a well-defined and clean way.
It is important to decide into which of these categories a certain problem falls. For example, consider our old power function:
function power(base, exponent) {
var result = 1;
for (var count = 0; count < exponent; count++)
result *= base;
return result;
}
When some geek tries to call power("Rabbit", 4), that is quite obviously a programmer error, but how about power(9, 0.5)? The function can not handle fractional exponents, but, mathematically speaking, raising a number to the halfth power is perfectly reasonable (Math.pow can handle it). In situations where it is not entirely clear what kind of input a function accepts, it is often a good idea to explicitly state the kind of arguments that are acceptable in a comment.
If a function encounters a problem that it can not solve itself, what should it do? In Data structures: Objects and Arrays we wrote the function between:
function between(string, start, end) {
var startAt = string.indexOf(start) + start.length;
var endAt = string.indexOf(end, startAt);
return string.slice(startAt, endAt);
}
If the given start and end do not occur in the string, indexOf will return -1 and this version of between will return a lot of nonsense: between("Your mother!", "{-", "-}") returns "our mother".
When the program is running, and the function is called like that, the code that called it will get a string value, as it expected, and happily continue doing something with it. But the value is wrong, so whatever it ends up doing with it will also be wrong. And if you are unlucky, this wrongness only causes a problem after having passed through twenty other functions. In cases like that, it is extremely hard to find out where the problem started.
In some cases, you will be so unconcerned about these problems that you don’t mind the function misbehaving when given incorrect input. For example, if you know for sure the function will only be called from a few places, and you can prove that these places give it decent input, it is generally not worth the trouble to make the function bigger and uglier so that it can handle problematic cases.
But most of the time, functions that fail “silently” are hard to use, and even dangerous. What if the code calling between wants to know whether everything went well? At the moment, it can not tell, except by re-doing all the work that between did and checking the result of between with its own result. That is bad. One solution is to make between return a special value, such as false or undefined, when it fails.
function between(string, start, end) {
var startAt = string.indexOf(start);
if (startAt == -1)
return undefined;
startAt += start.length;
var endAt = string.indexOf(end, startAt);
if (endAt == -1)
return undefined;
return string.slice(startAt, endAt);
}
You can see that error checking does not generally make functions prettier. But now code that calls between can do something like:
var input = prompt("Tell me something", "");
var parenthesized = between(input, "(", ")");
if (parenthesized != undefined)
print("You parenthesized '", parenthesized, "'.");
In many cases returning a special value is a perfectly fine way to indicate an error. It does, however, have its downsides. Firstly, what if the function can already return every possible kind of value? For example, consider this function that gets the last element from an array:
function lastElement(array) {
if (array.length > 0)
return array[array.length - 1];
else
return undefined;
}
alert(lastElement([1, 2, undefined]));
So did the array have a last element? Looking at the value lastElement returns, it is impossible to say.
The second issue with returning special values is that it can sometimes lead to a whole lot of clutter. If a piece of code calls between ten times, it has to check ten times whether undefined was returned. Also, if a function calls between but does not have a strategy to recover from a failure, it will have to check the return value of between, and if it is undefined, this function can then return undefined or some other special value to its caller, who in turn also checks for this value.
Sometimes, when something strange occurs, it would be practical to just stop doing what we are doing and immediately jump back to a place that knows how to handle the problem.
Well, we are in luck, a lot of programming languages provide such a thing. Usually, it is called exception handling and the “bad” thing that occurs in the program is called an exception.
The theory behind exception handling goes like this: It is possible for code to raise (or throw) an exception, which is a value. Raising an exception somewhat resembles a super-charged return from a function ― it does not just jump out of the current function, but also out of its callers, all the way up to the top-level call that started the current execution. This is called unwinding the stack. You may remember the stack of function calls that was mentioned in Functions. An exception zooms down this stack, throwing away all the call contexts it encounters.
If they always zoomed right down to the base of the stack, exceptions would not be of much use. They would just provide a novel way to blow up your program. Fortunately, it is possible to set obstacles for exceptions along the stack. These ‘catch’ the exception as it is zooming down, and can do something with it, after which the program continues running at the point where the exception was caught.
An example:
function lastElement(array) {
if (array.length > 0)
return array[array.length - 1];
else
throw "Can not take the last element of an empty array.";
}
function lastElementPlusTen(array) {
return lastElement(array) + 10;
}
try {
alert(lastElementPlusTen([]));
}
catch (error) {
alert("Something went wrong: ", error);
}
throw is the keyword that is used to raise an exception. The keyword try sets up an obstacle for exceptions: When the code in the block after it raises an exception, the catch block will be executed. The variable named in parentheses after the word catch is the name given to the exception value inside this block.
Note that the function lastElementPlusTen completely ignores the possibility that lastElement might go wrong. This is the big advantage of exceptions ― error-handling code is only necessary at the point where the error occurs, and the point where it is handled. The functions in between can forget all about it.
Well, almost.
Consider the following: A function processThing wants to set a top-level variable currentThing to point to a specific thing while its body executes, so that other functions can have access to that thing too. Normally you would of course just pass the thing as an argument, but assume for a moment that that is not practical. When the function finishes, currentThing should be set back to null.
var currentThing = null;
function processThing(thing) {
if (currentThing != null)
throw "Oh no! We are already processing a thing!";
currentThing = thing;
/* do complicated processing... */
currentThing = null;
}
But what if the complicated processing raises an exception? In that case the call to processThing will be thrown off the stack by the exception, and currentThing will never be reset to null.
try statements can also be followed by a finally keyword, which means ‘no matter what happens, run this code after trying to run the code in the try block’. If a function has to clean something up, the cleanup code should usually be put into a finally block:
function processThing(thing) {
if (currentThing != null)
throw "Oh no! We are already processing a thing!";
currentThing = thing;
try {
/* do complicated processing... */
}
finally {
currentThing = null;
}
}
A lot of errors in programs cause the JavaScript environment to raise an exception. For example:
try {
alert(Sasquatch);
}
catch (error) {
alert("Caught: " + error.message);
}
In cases like this, special error objects are raised. These always have a message property containing a description of the problem. You can raise similar objects using the new keyword and the Error constructor:
throw new Error("Fire!");
When an exception goes all the way to the bottom of the stack without being caught, it gets handled by the environment. What this means differs between the different browsers, sometimes a description of the error is written to some kind of log, sometimes a window pops up describing the error.
The errors produced by entering code in the console on this page are always caught by the console, and displayed among the other output.
Most programmers consider exceptions purely an error-handling mechanism. In essence, though, they are just another way of influencing the control flow of a program. For example, they can be used as a kind of break statement in a recursive function. Here is a slightly strange function which determines whether an object, and the objects stored inside it, contain at least seven true values:
var FoundSeven = {};
function hasSevenTruths(object) {
var counted = 0;
function count(object) {
for (var name in object) {
if (object[name] === true) {
counted++;
if (counted == 7)
throw FoundSeven;
}
else if (typeof object[name] == "object") {
count(object[name]);
}
}
}
try {
count(object);
return false;
}
catch (exception) {
if (exception != FoundSeven)
throw exception;
return true;
}
}
The inner function count is recursively called for every object that is part of the argument. When the variable counted reaches seven, there is no point in continuing to count, but just returning from the current call to count will not necessarily stop the counting, since there might be more calls below it. So what we do is just throw a value, which will cause the control to jump right out of any calls to count, and land at the catch block.
But just returning true in case of an exception is not correct. Something else might be going wrong, so we first check whether the exception is the object FoundSeven, created specifically for this purpose. If it is not, this catch block does not know how to handle it, so it raises it again.
This is a pattern that is also common when dealing with error conditions ― you have to make sure that your catch block only handles exceptions that it knows how to handle. Throwing string values, as some of the examples in this chapter do, is rarely a good idea, because it makes it hard to recognise the type of the exception. A better idea is to use unique values, such as the FoundSeven object, or to introduce a new type of objects, as described in Object-oriented Programming.