Exceptions and Error Handling - Learning JavaScript (2016)

Learning JavaScript (2016)

Chapter 11. Exceptions and Error Handling

As much as we would all like to live in an error-free world, we don’t have that luxury. Even the most trivial applications are subject to errors arising from conditions you didn’t anticipate. The first step to writing robust, high-quality software is acknowledging that it will have errors. The second step is anticipating those errors and handling them in a reasonable fashion.

Exception handling is a mechanism that came about to deal with errors in a controlled fashion. It’s called exception handling (as opposed to error handling) because it’s meant to deal with exceptional circumstances—that is, not the errors you anticipate, but the ones you don’t.

The line between anticipated errors and unanticipated errors (exceptions) is a blurry one that is very much situational. An application that is designed to be used by the general, untrained public may anticipate a lot more unpredictable behavior than an application designed to be used by trained users.

An example of an anticipated error is someone providing an invalid email address on a form: people make typos all the time. An unanticipated error might be running out of disk space, or a usually reliable service being unavailable.

The Error Object

JavaScript has a built-in Error object, which is convenient for any kind of error handling (exceptional or anticipated). When you create an instance of Error, you can provide an error message:

const err = new Error('invalid email');

Creating an Error instance doesn’t, by itself, do anything. What it does is give you something that can be used to communicate errors. Imagine a function that validates an email address. If the function is successful, it returns the email address as a string. If it isn’t, it returns an instance ofError. For the sake of simplicity, we’ll treat anything that has an at sign (@) in it as a valid email address (see Chapter 17):

function validateEmail(email) {

return email.match(/@/) ?

email :

new Error(`invalid email: ${email}`);

}

To use this, we can use the typeof operator to determine if an instance of Error has been returned. The error message we provided is available in a property message:

const email = "jane@doe.com";

const validatedEmail = validateEmail(email);

if(validatedEmail instanceof Error) {

console.error(`Error: ${validatedEmail.message});

} else {

console.log(`Valid email: ${validatedEmail}`);

}

While this is a perfectly valid and useful way to use the Error instance, it is more often used in exception handling, which we’ll cover next.

Exception Handling with try and catch

Exception handling is accomplished with a try...catch statement. The idea is that you “try” things, and if there were any exceptions, they are “caught.” In our previous example, validateEmail handles the anticipated error of someone omitting an at-sign in their email address, but there is also the possibility of an unanticipated error: a wayward programmer setting email to something that is not a string. As written, our previous example setting email to null, or a number, or an object—anything but a string—will cause an error, and your program will halt in a very unfriendly fashion. To safeguard against this unanticipated error, we can wrap our code in a try...catch statement:

const email = null; // whoops

try {

const validatedEmail = validateEmail(email);

if(validatedEmail instanceof Error) {

console.error(`Error: ${validatedEmail.message});

} else {

console.log(`Valid email: ${validatedEmail}`);

}

} catch(err) {

console.error(`Error: ${err.message}`);

}

Because we caught the error, our program will not halt—we log the error and continue. We may still be in trouble—if a valid email is required, our program might not be able to meaningfully continue, but at least we can now handle the error more gracefully.

Note that control shifts to the catch block as soon as an error occurs; that is, the if statement that follows the call to validateEmail won’t execute. You can have as many statements as you wish inside the try block; the first one that results in an error will transfer control to the catchblock. If there are no errors, the catch block is not executed, and the program continues.

Throwing Errors

In our previous example, we used a try...catch statement to catch an error that JavaScript itself generated (when we tried to call the match method of something that’s not a string). You can also “throw” (or “raise”) errors yourself, which initiates the exception handling mechanism.

Unlike other languages with exception handling, in JavaScript, you can throw any value: a number or a string, or any other type. However, it’s conventional to throw an instance of Error. Most catch blocks expect an instance of Error. Keep in mind that you can’t always control where the errors you throw are caught (functions you write might be used by another programmer, who would reasonbly expect any errors thrown to be instances of Error).

For example, if you’re writing a bill-pay feature for a banking application, you might throw an exception if the account balance can’t cover the bill (this is exceptional because this situation should have been checked before bill pay was initiated):

function billPay(amount, payee, account) {

if(amount > account.balance)

throw new Error("insufficient funds");

account.transfer(payee, amount);

}

When you call throw, the current function immediately stops executing (so, in our example, account.transfer won’t get called, which is what we want).

Exception Handling and the Call Stack

A typical program will call functions, and those functions will in turn call other functions, and those functions even more functions, and so on. The JavaScript interpreter has to keep track of all of this. If function a calls function b and function b calls function c, when function c finishes, control is returned to function b, and when b finishes, control is returned to function a. When c is executing, therefore, neither a nor b is “done.” This nesting of functions that are not done is called the call stack.

If an error occurs in c, then, what happens to a and b? As it happens, it causes an error in b (because b may rely on the return of c), which in turn causes an error in a (because a may rely on the return of b). Essentially, the error propagates up the call stack until it’s caught.

Errors can be caught at any level in the call stack; if they aren’t caught, the JavaScript interpreter will halt your program unceremoniously. This is called an unhandled exception or an uncaught exception, and it causes a program to crash. Given the number of places errors can occur, it’s difficult and unwieldy to catch all possible errors, which is why programs crash.

When an error is caught, the call stack provides useful information in diagnosing the problem. For example, if function a calls function b, which calls function c, and the error occurs in c, the call stack tells you that not only did the error occur in c, it occurred when it was called by b when bwas called by a. This is helpful information if function c is called from many different places in your program.

In most implementations of JavaScript, instances of Error contain a property stack, which is a string representation of the stack (it is a nonstandard feature of JavaScript, but it is available in most environments). Armed with this knowledge, we can write an example that demonstrates exception handling:

function a() {

console.log('a: calling b');

b();

console.log('a: done');

}

function b() {

console.log('b: calling c');

c();

console.log('b: done');

}

function c() {

console.log('c: throwing error');

throw new Error('c error');

console.log('c: done');

}

function d() {

console.log('d: calling c');

c();

console.log('d: done');

}

try {

a();

} catch(err) {

console.log(err.stack);

}

try {

d();

} catch(err) {

console.log(err.stack);

}

Running this example in Firefox yields the following console output:

a: calling b

b: calling c

c: throwing error

c@debugger eval code:13:1

b@debugger eval code:8:4

a@debugger eval code:3:4

@debugger eval code:23:4

d: calling c

c: throwing error

c@debugger eval code:13:1

d@debugger eval code:18:4

@debugger eval code:29:4

The lines with the at signs in them are the stack traces, starting with the “deepest” function (c), and ending with no function at all (the browser itself). You can see that we have two different stack traces: one showing that we called c from b, which was called from a, and one that c was called directly from d.

try...catch...finally

There are times when the code in a try block involves some sort of resource, such as an HTTP connection or a file. Whether or not there is an error, we want to free this resource so that it’s not permanently tied up by your program. Because the try block can contain as many statements as you want, any one of which can result in an error, it’s not a safe place to free the resource (because the error could happen before we have the chance to do so). It’s also not safe to free the resource in the catch block, because then it won’t get freed if there is no error. This is exactly the situation that demands the finally block, which gets called whether or not there is an error.

Because we haven’t covered file handling or HTTP connections yet, we’ll simply use an example with console.log statements to demonstrate the finally block:

try {

console.log("this line is executed...");

throw new Error("whoops");

console.log("this line is not...");

} catch(err) {

console.log("there was an error...");

} finally {

console.log("...always executed");

console.log("perform cleanup here");

}

Try this example with and without the throw statement; you will see that the finally block is executed in either case.

Let Exceptions Be Exceptional

Now that you know what exception handling is and how to do it, you might be tempted to use it for all of your error handling—both the common errors you anticipate, and the errors that you don’t. Throwing an error is, after all, extremely easy, and it’s a convenient way to “give up” when you encounter a condition that you don’t know how to handle. But exception handling comes at a cost. In addition to the risk of the exception never being caught (thereby crashing your program), exceptions carry a certain computational cost. Because exceptions have to “unwind” the stack trace until a catch block is encountered, the JavaScript interpreter has to do extra housekeeping. With ever-increasing computer speeds, this becomes less and less of a concern, but throwing exceptions in frequently used execution paths can cause a performance issue.

Remember that every time you throw an exception, you have to catch it (unless you want your program to crash). You can’t get something for nothing. It’s best to leave exceptions as a last line of defense, for handling the exceptional errors you can’t anticipate, and to manage anticipated errors with control flow statements.