21
Error Handling and Debugging

WHAT'S IN THIS CHAPTER?

  • Understanding browser error reporting
  • Handling errors
  • Debugging JavaScript code

WROX.COM DOWNLOADS FOR THIS CHAPTER

Please note that all the code examples for this chapter are available as a part of this chapter's code download on the book's website at www.wrox.com/go/projavascript4e on the Download Code tab.

JavaScript has traditionally been known as one of the most difficult programming languages to debug because of its dynamic nature and years without proper development tools. Errors typically resulted in confusing browser messages such as "object expected" that provided little or no contextual information. The third edition of ECMAScript aimed to improve this situation, introducing the try-catch and throw statements, along with various error types to help developers deal with errors when they occur. A few years later, JavaScript debuggers and debugging tools began appearing for web browsers. By 2008, most web browsers supported some JavaScript debugging capabilities.

Armed with the proper language support and development tools, web developers are now empowered to implement proper error-handling processes and figure out the cause of problems.

BROWSER ERROR REPORTING

All of the major desktop web browsers—Internet Explorer/Edge, Firefox, Safari, Chrome, and Opera—have some way to report JavaScript errors to the user. By default, all browsers hide this information, both because it's of little use to anyone but the developer, and because it's the nature of web pages to throw errors during normal operation.

Desktop Consoles

All modern desktop web browsers expose errors through their web console. These errors can be revealed in the developer tools console. In all the previously mentioned browsers, they share a common path to accessing the web console. Perhaps the easiest way to view errors is to right-click on the web page, select Inspect or Inspect Element, and click the Console tab.

To proceed directly to the console, different operating systems and browsers support different key combinations:

BROWSER WINDOWS/LINUX MAC
Chrome Ctrl+Shift+J Cmd+Opt+J
Firefox Ctrl+Shift+K Cmd+Opt+K
Internet Explorer/Edge F12, then Ctrl+2 NA
Opera Ctrl+Shift+I Cmd+Opt+I
Safari NA Cmd+Opt+C

Mobile Consoles

Natively, mobile phones will not offer a console interface directly on the device. However, there are several options for you to use in situations where you would like to inspect errors that are thrown on a mobile device.

Chrome for mobile and Safari on iOS come bundled with utilities that allow you to connect the device to a host operating system running that same browser, and you can then view the errors thrown through the paired desktop browser. This involves physically connecting the device and following the setup instructions for Chrome (https://developers.google.com/web/tools/chrome-devtools/remote-debugging/) or Safari (https://developer.apple.com/library/content/documentation/AppleApplications/Conceptual/Safari_Developer_Guide/GettingStarted/GettingStarted.html).

It's also possible to use a third-party utility to debug directly on the mobile device. A commonly used utility for debugging Firefox is Firebug Lite (https://getfirebug.com/firebuglite_mobile). This works by shimming the Firebug script into the current web page using a JavaScript bookmarklet. Once the script runs, you'll be able to open a debug interface right on the mobile browser. Firebug Lite also has versions for different browsers such as Chrome (http://getfirebug.com/releases/lite/chrome/).

ERROR HANDLING

No one doubts the importance of error handling in programming. Every major web application needs a good error-handling protocol and most good applications have one, though it is typically on the server side of the application. In fact, great care is usually taken by the server-side team to define an error-logging mechanism that categorizes errors by type, frequency, and any other metric that may be important. The result is the ability to understand how the application is working in the public with a simple database query or report-generating script.

Error handling has slowly been adopted on the browser side of web applications even though it is just as important. An important fact to understand is that most people who use the web are not technically savvy—most don't even fully comprehend what a web browser is, let alone which one they're using. As described earlier in this chapter, each browser behaves a little bit differently when a JavaScript error occurs. The default browser experience for JavaScript errors is horrible for the end user. In the best case, the user has no idea what happened and will try again; in the worst case, the user gets incredibly annoyed and never comes back. Having a good error-handling strategy keeps your users informed about what is going on without scaring them. To accomplish this, you must understand the various ways that you can trap and deal with JavaScript errors as they occur.

The try-catch Statement

ECMA-262, third edition, introduced the try-catch statement as a way to handle exceptions in JavaScript. The basic syntax is as follows, which is the same as the try-catch statement in Java:

try {
 // code that may cause an error
} catch (error) {
 // what to do when an error occurs
}

Any code that might possibly throw an error should be placed in the try portion of the statement, and the code to handle the error is placed in the catch portion, as shown in the following example:

try {
 window.someNonexistentFunction();
} catch (error){
 console.log("An error happened!");
}

If an error occurs at any point in the try portion of the statement, code execution immediately exits and resumes in the catch portion. The catch portion of the statement receives an object containing information about the error that occurred. Unlike other languages, you must define a name for the error object even if you don't intend to use it. The exact information available on this object varies from browser to browser but contains, at a minimum, a message property that holds the error message. ECMA-262 also specifies a name property that defines the type of error; this property is available in all current browsers. You can, therefore, display the actual browser message if necessary, as shown in the following example:

try {
 window.someNonexistentFunction();
} catch (error){
 console.log(error.message);
}

This example uses the message property when displaying an error message to the user. The message property is the only one that is guaranteed to be there across Internet Explorer, Firefox, Safari, Chrome, and Opera, even though each browser adds other information. Internet Explorer adds a description property, which is always equal to the message, as well as a number property, which gives an internal error number. Firefox adds fileName, lineNumber, and stack (which contains a stack trace). Safari adds line (for the line number), sourceId (an internal error code), and sourceURL. Once again, it is best to rely only on the message property for cross-browser compatibility.

The finally Clause

The optional finally clause of the try-catch statement always runs its code no matter what. If the code in the try portion runs completely, the finally clause executes; if there is an error and the catch portion executes, the finally portion still executes. There is literally nothing that can be done in the try or catch portion of the statement to prevent the code in finally from executing, which includes using a return statement. Consider the following function:

function testFinally(){
 try {
  return 2;
 } catch (error){
  return 1;
 } finally {
  return 0;
 }
}

This function simply places a return statement in each portion of the try-catch statement. It looks like the function should return 2 because that is in the try portion and wouldn't cause an error. However, the presence of the finally clause causes that return to be ignored; the function returns 0 when called no matter what. If the finally clause were removed, the function would return 2. If finally is provided, then catch becomes optional (only one or the other is required).

Error Types

Several different types of errors can occur during the course of code execution. Each error type has a corresponding object type that is thrown when an error occurs. ECMA-262 defines the following eight error types:

  • Error
  • InternalError
  • EvalError
  • RangeError
  • ReferenceError
  • SyntaxError
  • TypeError
  • URIError

The Error type is the base type from which all other error types inherit. As a result of this, all error types share the same properties (the only methods on error objects are the default object methods). An error of type Error is rarely, if ever, thrown by a browser; it is provided mainly for developers to throw custom errors.

The InternalError type is thrown when the underlying JavaScript engine throws an exception—for example, when a stack overflow occurs from too much recursion. This is not an error type that you would explicitly handle inside your code; if this error is thrown, chances are very good that your code is doing something incorrect or dangerous and should be fixed.

The EvalError type is thrown when an exception occurs while using the eval() function. ECMA-262 states that this error is thrown “if value of the eval property is used in any way other than a direct call (that is, other than by the explicit use of its name as an Identifier, which is the MemberExpression in a CallExpression), or if the eval property is assigned to.” This basically means using eval() as anything other than a function call, such as:

new eval(); // throws EvalError
eval = foo; // throws EvalError

In practice, browsers don't always throw EvalError when they're supposed to. For example, Firefox and Internet Explorer throw a TypeError in the first case but the second succeeds without error. Because of this and the unlikelihood of these patterns being used, it is highly unlikely that you will run into this error type.

A RangeError occurs when a number is outside the bounds of its range. For example, this error may occur when an attempt is made to define an array with an unsupported number of items, such as –20 or Number.MAX_VALUE, as shown here:

let items1 = new Array(-20);              // throws RangeError
let items2 = new Array(Number.MAX_VALUE); // throws RangeError 

Range errors occur infrequently in JavaScript.

The ReferenceError type is used when an object is expected. (This is literally the cause of the famous "object expected" browser error.) This type of error typically occurs when attempting to access a variable that doesn't exist, as in this example:

let obj = x; // throws ReferenceError when x isn't declared

A SyntaxError object is thrown most often when there is a syntax error in a JavaScript string that is passed to eval(), as in this example:

eval("a ++ b"); // throws SyntaxError

Outside of using eval(), the SyntaxError type is rarely used, because syntax errors occurring in JavaScript code stop execution immediately.

The TypeError type is the most used in JavaScript and occurs when a variable is of an unexpected type or an attempt is made to access a nonexistent method. This can occur for any number of reasons, most often when a type-specific operation is used with a variable of the wrong type. Here are some examples:

let o = new 10;                           // throws TypeError
console.log("name" in true);                   // throws TypeError
Function.prototype.toString.call("name"); // throws TypeError 

Type errors occur most frequently with function arguments that are used without their type being verified first.

The last type of error is URIError, which occurs only when using the encodeURI() or decodeURI() with a malformed URI. This error is perhaps the most infrequently observed in JavaScript, because these functions are incredibly robust.

The different error types can be used to provide more information about an exception, allowing appropriate error handling. You can determine the type of error thrown in the catch portion of a try-catch statement by using the instanceof operator, as shown here:

try {
 someFunction();
} catch (error){
 if (error instanceof TypeError){
  // handle type error
 } else if (error instanceof ReferenceError){
  // handle reference error
 } else {
  // handle all other error types
 }
}

Checking the error type is the easiest way to determine the appropriate course of action in a cross-browser way because the error message contained in the message property differs from browser to browser.

Usage of try-catch

When an error occurs within a try-catch statement, the browser considers the error to have been handled, and so it won't report it using the mechanisms discussed earlier in this chapter. This is ideal for web applications with users who aren't technically inclined and wouldn't otherwise understand when an error occurs. The try-catch statement allows you to implement your own error-handling mechanism for specific error types.

The try-catch statement is best used where an error might occur that is out of your control. For example, if you are using a function that is part of a larger JavaScript library, that function may throw errors either purposefully or by mistake. Because you can't modify the library's code, it would be appropriate to surround the call in a try-catch statement in case an error does occur and then handle the error appropriately.

It's not appropriate to use a try-catch statement if you know an error will occur with your code specifically. For example, if a function will fail when a string is passed in instead of a number, you should check the data type of the argument and act accordingly; there is no need in this case to use a try-catch statement.

Throwing Errors

A companion to the try-catch statement is the throw operator, which can be used to throw custom errors at any point in time. The throw operator must be used with a value but places no limitation on the type of value. All of the following lines are legal:

throw 12345;
throw "Hello world!";
throw true;
throw { name: "JavaScript" };

When the throw operator is used, code execution stops immediately and continues only if a try-catch statement catches the value that was thrown.

Browser errors can be more accurately simulated by using one of the built-in error types. Each error type's constructor accepts a single argument, which is the exact error message. Here is an example:

throw new Error("Something bad happened.");

This code throws a generic error with a custom error message. The error is handled by the browser as if it were generated by the browser itself, meaning that it is reported by the browser in the usual way and your custom error message is displayed. You can achieve the same result using the other error types, as shown in these examples:

throw new SyntaxError("I don't like your syntax."); 
throw new InternalError("I can't do that, Dave.");
throw new TypeError("What type of variable do you take me for?");
throw new RangeError("Sorry, you just don't have the range.");
throw new EvalError("That doesn't evaluate.");
throw new URIError("Uri, is that you?");
throw new ReferenceError("You didn't cite your references properly.");

The most often used error types for custom error messages are Error, RangeError, ReferenceError, and TypeError.

You can also create custom error types by inheriting from Error (discussed in Chapter 6). You should provide both a name property and a message property on your error type. Here is an example:

class CustomError extends Error {
 constructor(message) {
  super(message);
  this.name = "CustomError";
  this.message = message;
 }
}
          
throw new CustomError("My message");

Custom error types that are inherited from Error are treated just like any other error by the browser. Creating custom error types is helpful when you will be catching the errors that you throw and need to decipher them from browser-generated errors.

When to Throw Errors

Throwing custom errors is a great way to provide more information about why a function has failed. Errors should be thrown when a particular known error condition exists that won't allow the function to execute properly. That is, the browser will throw an error while executing this function given a certain condition. For example, the following function will fail if the argument is not an array:

function process(values){
 values.sort();
         
 for (let value of values){
  if (value> 100){
   return value;
  }
 }
          
 return -1;
}

If this function is run with a string as the argument, the call to sort() fails. Each browser gives a different, though somewhat obtuse, error message, as listed here:

  • Internet Explorer—Property or method doesn't exist.
  • Firefoxvalues.sort() is not a function.
  • Safari—Value undefined (result of expression values.sort) is not an object.
  • Chrome—Object name has no method 'sort'.
  • Opera—Type mismatch (usually a non-object value used where an object is required).

Although Firefox, Chrome, and Safari at least indicate the part of the code that caused the error, none of the error messages are particularly clear as to what happened or how it could be fixed. When dealing with one function, as in the preceding example, debugging is easy enough to handle with these error messages. However, when you're working on a complex web application with thousands of lines of JavaScript code, finding the source of the error becomes much more difficult.

This is where a custom error with appropriate information will significantly contribute to the maintainability of the code. Consider the following example:

function process(values){ 
 if (!(values instanceof Array)){
  throw new Error("process(): Argument must be an array.");
 }

 values.sort();

 for (let value of values){
  if (value> 100){
   return value;
  }
 }

 return -1;
}

In this rewritten version of the function, an error is thrown if the values argument isn't an array. The error message provides the name of the function and a clear description as to why the error occurred. If this error occurred in a complex web application, you would have a much clearer idea of where the real problem is.

When you're developing JavaScript code, critically evaluate each function and the circumstances under which it may fail. A good error-handling protocol ensures that the only errors that occur are the ones that you throw.

Throwing Errors versus try-catch

A common question that arises is when to throw errors versus when to use try-catch to capture them. Generally speaking, errors are thrown in the low levels of an application architecture, at a level where not much is known about the ongoing process, and so the error can't really be handled. If you are writing a JavaScript library that may be used in a number of different applications, or even a utility function that will be used in a number of different places in a single application, you should strongly consider throwing errors with detailed information. It is then up to the application to catch the errors and handle them appropriately.

The best way to think about the difference between throwing errors and catching errors is this: you should catch errors only if you know exactly what to do next. The purpose of catching an error is to prevent the browser from responding in its default manner; the purpose of throwing an error is to provide information about why an error occurred.

The error Event

Any error that is not handled by a try-catch causes the error event to fire on the window object. This event was one of the first supported by web browsers, and its format has remained intact for backwards compatibility in all major browsers. An onerror event handler doesn't create an event object in any browser. Instead, it receives three arguments: the error message, the URL on which the error occurred, and the line number. In most cases, only the error message is relevant because the URL is the same as the location of the document, and the line number could be for inline JavaScript or code in external files. The onerror event handler needs to be assigned using the DOM Level 0 technique shown here because it doesn't follow the DOM Level 2 Events standard format:

window.onerror = (message, url, line) => {
 console.log(message);
};

When any error occurs, whether browser-generated or not, the error event fires, and this event handler executes. Then, the default browser behavior takes over, displaying the error message as it would normally. You can prevent the default browser error reporting by returning false, as shown here:

window.onerror = (message, url, line) => {
 console.log(message);
 return false;
};

By returning false, this function effectively becomes a try-catch statement for the entire document, capturing all unhandled runtime errors. This event handler is the last line of defense against errors being reported by the browser and, ideally, should never have to be used. Proper usage of the try-catch statement means that no errors reach the browser level and, therefore, should never fire the error event.

Images also support an error event. Any time the URL in an image's src attribute doesn't return a recognized image format, the error event fires. This event follows the DOM format by returning an event object with the image as the target. Here is an example:

const image = new Image();

image.addEventListener("load", (event) => {
 console.log("Image loaded!");
});
image.addEventListener("error", (event) => {
 console.log("Image not loaded!");
});

image.src = "doesnotexist.gif"; // does not exist, resoure will fail to load

In this example, an alert is displayed when the image fails to load. It's important to understand that once the error event fires, the image download process is already over and will not be resumed.

Error-Handling Strategies

Error-handling strategies have traditionally been confined to the server for web applications. There's often a lot of thought that goes into errors and error handling, including logging and monitoring systems. The point of such tools is to analyze error patterns in the hopes of tracking down the root cause and understanding how many users the error affects.

It is equally important to have an error-handling strategy for the JavaScript layer of a web application. Because any JavaScript error can cause a web page to become unusable, understanding when and why errors occur is vital. Most web-application users are not technical and can easily get confused when something doesn't work as expected. They may reload the page in an attempt to fix the problem, or they may just stop trying. As the developer, you should have a good understanding of when and how the code could fail and have a system to track such issues.

Identify Where Errors Might Occur

The most important part of error handling is to first identify where errors might occur in the code. Since JavaScript is loosely typed and function arguments aren't verified, there are often errors that become apparent only when the code is executed. In general, there are three error categories to watch for:

  • Type coercion errors
  • Data type errors
  • Communication errors

Each of these errors occurs when using specific patterns or not applying sufficient value checking.

Static Code Analyzer

It would be irresponsible to not mention here that an overwhelming abundance of errors you might encounter can be preemptively handled by using a static code analyzer or linter as part of your application build process. There is a terrific list of resources at https://gist.github.com/listochkin/6250151. The most commonly used static analyzers are JSHint, JSLint, Google Closure, and TypeScript.

Static code analyzers will require you to annotate your JavaScript with types, function signatures, and other directives that describe how the program will run outside of the base executable code. The analyzer will compare your annotations against various parts of your JavaScript codebase that use each other and make you aware of any potential incompatibilities that might manifest inside the actual runtime.

Type Coercion Errors

Type coercion errors occur as the result of using an operator or other language construct that automatically changes the data type of a value. The two most common type coercion errors occur as a result of using the equal (==) or not equal (!=) operator and using a non-Boolean value in a flow control statement, such as if, for, and while.

The equal and not equal operators, discussed in Chapter 3, automatically convert values of different types before performing a comparison. Since the same symbols typically perform straight comparisons in nondynamic languages, developers often mistakenly use them in JavaScript in the same way. In most cases, it's best to use the identically equal (===) and not identically equal (!==) operators to avoid type coercion. Here is an example:

console.log(5 == "5");  // true
console.log(5 === "5");  // false
console.log(1 == true);  // true
console.log(1 === true); // false

In this code, the number 5 and the string "5" are compared using the equal operator and the identically equal operator. The equal operator first converts the string "5" into the number 5 and then compares it with the other number 5, resulting in true. The identically equal operator notes that the two data types are different and simply returns false. The same occurs with the values 1 and true: they are considered equal by the equal operator but not equal using the identically equal operator. Using the identically equal and not identically equal operators can prevent type coercion errors that occur during comparisons and are highly recommended over using the equal and not equal operators.

Type coercion errors also occur in flow control statements. Statements such as if automatically convert any value into a Boolean before determining the next step. The if statement, specifically, is often used in error-prone ways. Consider the following example:

function concat(str1, str2, str3) {
 let result = str1 + str2;
 if (str3) { // avoid!!!
  result += str3;
 }
 return result;
}

This function's intended purpose is to concatenate two or three strings and return the result. The third string is an optional argument and so must be checked. As mentioned in Chapter 3, named variables that aren't used are automatically assigned the value of undefined. The value undefined converts into the Boolean value false, so the intent of the if statement in this function is to concatenate the third argument only if it is provided. The problem is that undefined is not the only value that gets converted to false, and a string is not the only value that gets converted to true. If the third argument is the number 0, for example, the if condition fails, while a value of 1 causes the condition to pass.

Using non-Boolean values as conditions in a flow control statement is a very common cause of errors. To avoid such errors, always make sure that a Boolean value is passed as the condition. This is most often accomplished by doing a comparison of some sort. For example, the previous function can be rewritten as shown here:

function concat(str1, str2, str3){
 let result = str1 + str2;
 if (typeof str3 === "string") { // proper comparison
  result += str3;
 }
 return result;
}

In this updated version of the function, the if statement condition returns a Boolean value based on a comparison. This function is much safer and is less affected by incorrect values.

Data Type Errors

Because JavaScript is loosely typed, variables and function arguments aren't compared to ensure that the correct type of data is being used. It is up to you, as the developer, to do an appropriate amount of data type checking to ensure that an error will not occur. Data type errors most often occur as a result of unexpected values being passed into a function.

In the previous example, the data type of the third argument is checked to ensure that it's a string, but the other two arguments aren't checked at all. If the function must return a string, then passing in two numbers and omitting the third argument easily breaks it. A similar situation is present in the following function:

// unsafe function, any non-string value causes an error
function getQueryString(url) {
 const pos = url.indexOf("?");
 if (pos> -1){
  return url.substring(pos +1);
 }
 return "";
} 

The purpose of this function is to return the query string of a given URL. To do so, it first looks for a question mark in the string using indexOf() and, if found, returns everything after the question mark using the substring() method. The two methods used in this example are specific to strings, so any other data type that is passed in will cause an error. The following simple type check makes this function less error prone:

function getQueryString(url) {
 if (typeof url === "string") { // safer with type check
  let pos = url.indexOf("?");
  if (pos> -1) {
   return url.substring(pos +1);
  } 
 }
 return "";
} 

In this rewritten version of the function, the first step is to check that the value passed in is actually a string. This ensures that the function will never cause an error because of a nonstring value.

As discussed in the previous section, using non-Boolean values as conditions for flow control statements is a bad idea because of type coercion. This is also a bad practice that can cause data type errors. Consider the following function:

// unsafe function, non-array values cause an error
function reverseSort(values) {
 if (values) { // avoid!!!
  values.sort();
  values.reverse();
 }
}

The reverseSort() function sorts an array in reverse order, using both the sort() and the reverse() methods. Because of the control condition in the if statement, any nonarray value that converts to true will cause an error. Another common mistake is to compare the argument against null, as in this example:

// still unsafe, non-array values cause an error
function reverseSort(values) {
 if (values != null){ // avoid!!!
  values.sort();
  values.reverse();
 }
}

Comparing a value against null only protects the code from two values: null and undefined (which are equivalent to using the equal and not equal operators). A null comparison doesn't do enough to ensure that the value is appropriate; therefore, this technique should be avoided. It's also recommended that you don't compare a value against undefined, for the same reason.

Another poor choice is to use feature detection for only one of the features being used. Here is an example:

// still unsafe, non-array values cause an error
function reverseSort(values) {
 if (typeof values.sort === "function") { // avoid!!!
  values.sort();
  values.reverse();
 }
}

In this example, the code checks for the existence of a sort() method on the argument. This leaves open the possibility that an object may be passed in with a sort() function that is not an array, in which case the call to reverse() causes an error. When you know the exact type of object that is expected, it's best to use instanceof, as shown in the following example, to determine that the value is of the right type:

// safe, non-array values are ignored
function reverseSort(values) {
 if (values instanceof Array) { // fixed
  values.sort();
  values.reverse();
 }
}

This last version of reverseSort() is safe—it tests the values argument to see if it's an instance of Array. In this way, the function is assured that any nonarray values are ignored.

Generally speaking, values that should be primitive types should be checked using typeof, and values that should be objects should be checked using instanceof. Depending on how a function is being used, it may not be necessary to check the data type of every argument, but any public-facing APIs should definitely perform type checking to ensure proper execution.

Communication Errors

Since the advent of Ajax programming, it has become quite common for web applications to dynamically load information or functionality throughout the application's life cycle. Any communication between JavaScript and the server is an opportunity for an error to occur.

The first type of communication error involves malformed URLs or post data. This typically occurs when data isn't encoded using encodeURIComponent() before being sent to the server. The following URL, for example, isn't formed correctly:

http://www.yourdomain.com/?redir=http://www.someotherdomain.com?a=b&c=d

This URL can be fixed by using encodeURIComponent() on everything after "redir=", which produces the following result:

http://www.example.com/?redir=http%3A%2F%2Fwww.someotherdomain.com%3Fa%3Db%26c%3Dd

The encodeURIComponent() method should always be used for query string arguments. To ensure that this happens, you will find it's sometimes helpful to define a function that handles query string building, such as the following:

function addQueryStringArg(url, name, value) {
 if (url.indexOf("?") == -1){
  url += "?";
 } else {
  url += "&";
 }
  
 url += '${encodeURIComponent(name)=${encodeURIComponent(value)}';
 return url;
}

This function accepts three arguments: the URL to append the query string argument to, the name of the argument, and the argument value. If the URL that's passed in doesn't contain a question mark, then one is added; otherwise, an ampersand is added because this means there are other query string arguments. The query string name and value are then encoded and added to the URL. The function can be used as in the following example:

const url = "http://www.somedomain.com";
const newUrl = addQueryStringArg(url, "redir", 
                 "http://www.someotherdomain.com?a=b&c=d");
console.log(newUrl);

Using this function instead of manually building URLs can ensure proper encoding and avoid errors related to it.

Communication errors also occur when the server response is not as expected. When using dynamic script loading or dynamic style loading, there is the possibility that the requested resource is not available. Some browsers fail silently when a resource isn't returned, whereas others error out. Unfortunately, there is little you can do when using these techniques to determine that an error has occurred. In some cases, using Ajax communication can provide additional information about error conditions.

Distinguishing between Fatal and Nonfatal Errors

One of the most important parts of any error-handling strategy is to determine whether or not an error is fatal. One or more of the following identifies a nonfatal error:

  • It won't interfere with the user's main tasks.
  • It affects only a portion of the page.
  • Recovery is possible.
  • Repeating the action may result in success.

In essence, nonfatal errors aren't a cause for concern. For example, Gmail (https://mail.google.com) has a feature that allows users to send Hangouts messages from the interface. If, for some reason, Hangouts don't work, it's a nonfatal error because that is not the application's primary function. The primary use case for Gmail is to read and write e-mail messages, and as long as the user can do that, there is no reason to interrupt the user experience. Nonfatal errors don't require you to send an explicit message to the user. You may be able to replace the area of the page that is affected with a message indicating that the functionality isn't available, but it's not necessary to interrupt the user.

Fatal errors, on the other hand, are identified by one or more of the following:

  • The application absolutely cannot continue.
  • The error significantly interferes with the user's primary objective.
  • Other errors will occur as a result.

It's vitally important to understand when a fatal error occurs in JavaScript so appropriate action can be taken. When a fatal error occurs, you should send a message to the users immediately to let them know that they will not be able to continue what they were doing. If the page must be reloaded for the application to work, then you should tell the user this and provide a button that automatically reloads the page.

You must also make sure that your code doesn't dictate what is and is not a fatal error. Nonfatal and fatal errors are primarily indicated by their affect on the user. Good code design means that an error in one part of the application shouldn't unnecessarily affect another part that, in reality, isn't related at all. For example, consider a personalized home page, such as Gmail (https://mail.google.com), that has multiple independent modules on the page. If each module has to be initialized using a JavaScript call, you may see code that looks something like this:

for (let mod of mods){
  mod.init(); // possible fatal error
}

On its surface, this code appears fine: the init() method is called on each module. The problem is that an error in any module's init() method will cause all modules that come after it in the array to never be initialized. If the error occurs on the first module, then none of the modules on the page will be initialized. Logically, this doesn't make sense because each module is an independent entity that isn't reliant on any other module for its functionality. It's the structure of the code that makes this type of error fatal. Fortunately, the code can be rewritten as follows to make an error in any one module nonfatal:

for (let mod of mods){
 try {
  mod.init();
 } catch (ex){
  // handle error here
 }
}

By adding a try-catch statement into the for loop, any error when a module initializes will not prevent other modules from initializing. When an error occurs in this code, it can be handled independently and in a way that doesn't interfere with the user experience.

Log Errors to the Server

A common practice in web applications is to have a centralized error log where important errors are written for tracking purposes. Database and server errors are regularly written to the log and categorized through some common API. With complex web applications, it's recommended that you also log JavaScript errors back to the server. The idea is to log the errors into the same system used for server-side errors and categorize them as having come from the front end. Using the same system allows for the same analytics to be performed on the data regardless of the error's source.

To set up a JavaScript error-logging system, you'll first need a page or entry point on the server that can handle the error data. The page need not do anything more than take data from the query string and save it to an error log. This page can then be used with code such as the following:

function logError(sev, msg) {
 let img = new Image(),
   encodedSev = encodeURIComponent(sev),
   encodedMsg = encodeURIComponent(msg);
 img.src = 'log.php?sev=${encodedSev}&msg=${encodedMsg}';
}

The logError() function accepts two arguments: a severity and the error message. The severity may be numbers or strings, depending on the system you're using. An Image object is used to send the request because of its flexibility, as described here:

  • The Image object is available in all browsers, even those that don't support the XMLHttpRequest object.
  • Cross-domain restrictions don't apply. Often there is one server responsible for handling error logging from multiple servers, and XMLHttpRequest would not work in that situation.
  • There's less chance that an error will occur in the process of logging the error. Most Ajax communication is handled through functionality wrappers provided by JavaScript libraries. If that library's code fails, and you're trying to use it to log the error, the message may never get logged.

Whenever a try-catch statement is used, it's likely that the error should be logged. Here is an example:

for (let mod of mods){
 try {
  mod.init();
 } catch (ex){
  logError("nonfatal", 'Module init failed: ${ex.message}');
 }
}

In this code, logError() is called when a module fails to initialize. The first argument is "nonfatal", indicating the severity of the error, and the message provides contextual information plus the true JavaScript error message. Error messages that are logged to the server should provide as much contextual information as possible to help identify the exact cause of the error.

DEBUGGING TECHNIQUES

Before JavaScript debuggers were readily available, developers had to use creative methods to debug their code. This led to the placement of code specifically designed to output debugging information in one or more ways. The most common debugging technique was to insert alerts throughout the code in question, which was both tedious, because it required cleanup after the code was debugged, and annoying if an alert was mistakenly left in code that was used in a production environment. Alerts are no longer recommended for debugging purposes, because several other, more elegant solutions are available.

Logging Messages to a Console

All major browsers have JavaScript consoles that can be used to view JavaScript errors. All three also allow you to write directly to the JavaScript console via the console object, which has the following methods:

  • error(message )—Logs an error message to the console
  • info(message )—Logs an informational message to the console
  • log(message )—Logs a general message to the console
  • warn(message )—Logs a warning message to the console

The message display on the error console differs according to the method that was used to log the message. Error messages contain a red icon, whereas warnings contain a yellow icon. Console messages may be used, as in the following function:

function sum(num1, num2){
 console.log('Entering sum(), arguments are ${num1},${num2}');
          
 console.log("Before calculation");
 const result = num1 + num2;
 console.log("After calculation");
          
 console.log("Exiting sum()");
 return result;
}

As the sum() function is called, several messages are output to the JavaScript console to aid in debugging.

Logging messages to the JavaScript console is helpful in debugging code, but all messages should be removed when code goes to production. This can be done automatically, using a code-processing step in deployment, or manually.

Understanding the Console Runtime

The browser console is a REPL (read-eval-print loop) that is concurrent with the page's JavaScript runtime. It behaves effectively the same way as if the browser were evaluating a newly discovered <script> tag inside the DOM. Commands executed from inside the console can access globals and various APIs in the same way that page-level JavaScript can. An arbitrary amount of code can be evaluated from the console; as is the case with any other page-level code it is blocking. Modifications, objects, and callbacks will persist to the DOM and/or runtime.

The JavaScript runtime will restrict what different windows can access, and so in all major browsers you are able to select which window the JavaScript console inputs should execute in. The code you execute does not execute with elevated privilege—it is still subject to cross-origin restrictions and any other controls that are enforced by the browser.

The console runtime also has developer tool integration that offers you some contextual bonus tools to help with debugging that are not available in normal JavaScript. One of the most useful tools is the last-clicked selector, which is available in some form in all major browsers. In the Element tab inside the developer tools, when you click a node in the DOM tree, you gain a reference to the JavaScript instance of that node inside the Console tab by using $0. It behaves as a normal JavaScript instance, so reading properties such as $0.scrollWidth and invoking member methods such as $0.remove() are allowed.

Using the JavaScript Debugger

Also available to you in all major browsers is the JavaScript debugger. As part of the ECMAScript 5.1 specification, the debugger keyword will attempt to invoke any available debugging functionality. If there is no associated behavior, this statement will be silently skipped as a no-op. The statement can be used as follows:

function pauseExecution(){
 console.log("Will print before breakpoint");
 debugger;
 console.log("Will not print until breakpoint continues");
} 

When the runtime encounters the keyword, in all major browsers it will open the developer tools panel with a breakpoint set at that exact point. You will then be able to use a separate browser console that executes code at the specific lexical scope in which the breakpoint is currently stopped. Additionally, you will be able to perform standard code debugger operations (step into, step over, continue, and so on).

Browsers will also commonly allow you to set breakpoints manually (without using the debugger keyword statement) by inspecting the actual loaded JavaScript code inside the developer tools and selecting the line at which you would like to set the breakpoint. This set breakpoint will behave in the same way, but it will not persist through browser sessions.

Logging Messages to the Page

Another common way to log debugging messages is to specify an area of the page that messages are written to. This may be an element that is included all the time but only used for debugging purposes, or an element that is created only when necessary. For example, the log() function may be changed to the following:

function log(message) {
 // Lexical scope of this function will use the following instance
 // instead of window.console
 const console = document.getElementById("debuginfo");
 if (console === null){
  console = document.createElement("div");
  console.id = "debuginfo";
  console.style.background = "#dedede";
  console.style.border = "1px solid silver";
  console.style.padding = "5px";
  console.style.width = "400px";
  console.style.position = "absolute";
  console.style.right = "0px";
  console.style.top = "0px";
  document.body.appendChild(console);
 }
 console.innerHTML += '<p> ${message}</p>';
}

In this new version of log(), the code first checks to see if the debugging element already exists. If not, then a new <div> element is created and assigned stylistic information to separate it from the rest of the page. After that, the message is written into the <div> using innerHTML. The result is a small area that displays log information on the page.

Shimming Console Methods

It is burdensome to a developer to remember to use two different types of log statements—the native console.log(), and a separately defined custom log(). Because console is a global object with writable member methods, it is completely possible to overwrite its member methods with custom behavior and allow log statements sprinkled throughout the codebase to happily log to whatever you have defined.

You can define a shim as follows:

// Join all arguments into string and alert the result
console.log = function() {
 // 'arguments' does not have a join method, first convert arguments to array
 const args = Array.prototype.slice.call(arguments);
 console.log(args.join(', '));
}

Now, this will be invoked instead of the conventional log behavior. This modification will not persist a page reload so it is a useful and lightweight strategy for debugging or log interception.

Throwing Errors

As mentioned earlier, throwing errors is an excellent way to debug code. If your error messages are specific enough, just seeing the error as it's reported may be enough to determine the error's source. The key to good error messages is for them to provide exact details about the cause of the error so that additional debugging is minimal. Consider the following function:

function divide(num1, num2) {
 return num1 / num2;
}

This simple function divides two numbers but will return NaN if either of the two arguments isn't a number. Simple calculations often cause problems in web applications when they return NaN unexpectedly. In this case, you can check that the type of each argument is a number before attempting the calculation. Consider the following example:

function divide(num1, num2) {
 if (typeof num1 != "number" || typeof num2 != "number"){
  throw new Error("divide(): Both arguments must be numbers.");
 }
 return num1 / num2;
}

Here, an error is thrown if either of the two arguments isn't a number. The error message provides the name of the function and the exact cause of the error. When the browser reports this error message, it immediately gives you a place to start looking for problems and a basic summary of the issue. This is much easier than dealing with a nonspecific browser error message.

In large applications, custom errors are typically thrown using an assert() function. Such a function takes a condition that should be true and throws an error if the condition is false. The following is a very basic assert() function:

function assert(condition, message) {
 if (!condition) {
  throw new Error(message);
 }
}

The assert() function can be used in place of multiple if statements in a function and can be a good location for error logging. This function can be used as follows:

function divide(num1, num2) {
 assert(typeof num1 == "number" && typeof num2 == "number", 
     "divide(): Both arguments must be numbers.");
 return num1 / num2;
}

Using an assert() function reduces the amount of code necessary to throw custom errors and makes the code more readable compared to the previous example.

COMMON LEGACY INTERNET EXPLORER ERRORS

Internet Explorer has traditionally been one of the most difficult browsers in which to debug JavaScript errors. Legacy versions of the browser throw error messages that are generally short and confusing, with little or no context given. The following sections provide a list of common and difficult-to-debug JavaScript errors that may occur in legacy versions of Internet Explorer. Because these browsers do not support ES6, the code will be backwards compatible.

Invalid Character

The syntax of a JavaScript file must be made up of certain characters. When an invalid character is detected in a JavaScript file, IE throws the "invalid character" error. An invalid character is any character not defined as part of JavaScript syntax. For example, there is a character that looks like a minus sign but is represented by the Unicode value 8211 (u2013). This character cannot be used in place of a regular minus sign (ASCII code 45) because it's not part of JavaScript syntax. This special character is often automatically inserted into Microsoft Word documents, so you will get an illegal character error if you were to copy code written in Word to a text editor and then run it in Internet Explorer. Other browsers react similarly. Firefox throws an "illegal character" error, Safari reports a syntax error, and Opera reports a ReferenceError, because it interprets the character as an undefined identifier.

Member Not Found

As mentioned previously, all DOM objects in Internet Explorer are implemented as COM objects rather than in native JavaScript. This can result is some very strange behavior when it comes to garbage collection. The "member not found" error is the direct result of the mismatched garbage collection routines in Internet Explorer.

This error typically occurs when you're trying to assign a value to an object property after the object has already been destroyed. The object must be a COM object to get this specified error message. The best example of this occurs when you are using the event object. The Internet Explorer event object exists as a property of window and is created when the event occurs and destroyed after the last event handler has been executed. So if you were to use the event object in a closure that was to be executed later, any attempt to assign to a property of event will result in this error, as in the following example:

document.onclick = function() {
 var event = window.event;
 setTimeout(function(){
  event.returnValue = false; // member not found error
 }, 1000);
}; 

In this code, a click handler is assigned to the document. It stores a reference to window.event in a local variable named event. This event variable is then referenced in a closure that is passed into setTimeout(). When the onclick event handler is exited, the event object is destroyed, so the reference in the closure is to an object whose members no longer exist. Assigning a value to returnValue causes the "member not found" error because you cannot write to a COM object that has already destroyed its members.

Unknown Runtime Error

An unknown runtime error occurs when HTML is assigned using the innerHTML or outerHTML property in one of the following ways: if a block element is being inserted into an inline element or you're accessing either property on any part of a table (<table>, <tbody>, and so on). For example, a <p> tag cannot technically contain a block-level element such as a <div>, so the following code will cause an unknown runtime error:

p.innerHTML = "<div>Hi</div>"; // where p contains a <p> element

Other browsers attempt to error-correct when block elements are inserted in invalid places so that no error occurs, but Internet Explorer is much stricter in this regard.

Syntax Error

Often when Internet Explorer reports a syntax error, the cause is immediately apparent. You can usually trace back the error to a missing semicolon or an errant closing brace. However, there is another instance where a syntax error occurs that may not be immediately apparent.

If you are referencing an external JavaScript file that for some reason returns non-JavaScript code, Internet Explorer throws a syntax error. For example, if you set the src attribute of a <script> to point to an HTML file, a syntax error occurs. The syntax error is typically reported as the first line and first character of a script. Opera and Safari report a syntax error as well, but they will also report the referenced file that caused the problem. Internet Explorer gives no such information, so you need to double-check every externally referenced JavaScript file. Firefox simply ignores any parsing errors in a non-JavaScript file that's included as if it were JavaScript.

This type of error typically occurs when JavaScript is being dynamically generated by a server-side component. Many server-side languages automatically insert HTML into the output if a runtime error occurs, and such output clearly breaks JavaScript syntax. If you're having trouble tracking down a syntax error, double-check each external JavaScript file to be sure that it doesn't contain HTML inserted by the server because of an error.

The System Cannot Locate the Resource Specified

Perhaps one of the least useful error messages is "The system cannot locate the resource specified." This error occurs when JavaScript is used to request a resource by URL and the URL is longer than Internet Explorer's maximum URL length of 2083 characters. This URL length limit applies not just to JavaScript but also to Internet Explorer in general. (Other browsers do not limit URL length so tightly.) There is also a URL path limit of 2048 characters. The following example causes this error:

function createLongUrl(url) {
 var s = "?";
 for (var i = 0, len = 2500; i < len; i++){
  s += "a";
 }
  
 return url + s;
}
          
var x = new XMLHttpRequest();
x.open("get", createLongUrl("http://www.somedomain.com/"), true);
x.send(null);

In this code, the XMLHttpRequest object attempts to make a request to a URL that exceeds the maximum URL limit. The error occurs when open() is called. One workaround for this type of error is to shorten the query string necessary for the request to succeed, either by decreasing the size of the named query string arguments or by eliminating unnecessary data. Another workaround is to change the request to a POST and send the data as the request body instead of in the query string.

SUMMARY

Error handling in JavaScript is critical for today's complex web applications. Failing to anticipate where errors might occur and how to recover from them can lead to a poor user experience and possibly frustrated users. Most browsers don't report JavaScript errors to users by default, so you need to enable error reporting when developing and debugging. In production, however, no errors should ever be reported this way.

The following methods can be used to prevent the browser from reacting to a JavaScript error:

  • The try-catch statement can be used where errors may occur, giving you the opportunity to respond to errors in an appropriate way instead of allowing the browser to handle the error.
  • Another option is to use the window.onerror event handler, which receives all errors that are not handled by a try-catch (Internet Explorer, Firefox, and Chrome only).

Each web application should be inspected to determine where errors might occur and how those errors should be dealt with.

  • A determination as to what constitutes a fatal error or a nonfatal error needs to be made ahead of time.
  • After that, code can be evaluated to determine where the most likely errors will occur. Errors commonly occur in JavaScript because of the following factors:
    • Type coercion
    • Insufficient data type checking
    • Incorrect data being sent to or received from the server

Internet Explorer, Firefox, Chrome, Opera, and Safari each have JavaScript debuggers that either come with the browser or can be downloaded as an add-on. Each debugger offers the ability to set breakpoints, control code execution, and inspect the value of variables at runtime.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.8.247