most browsers, errors can be caught by a top-level window.onerror() handler that provides a browser-spe-cific message, file, and line number.
Simply dumping these messages to a user-visible console represents a major step forward, but even an accurate message, file, and line number can be worthless when debugging a problem in an AJAX application. Unless the bug is a simple typographical error, we need to better understand the context in which the error was encountered.
Faced with an unexpected error, the next question is almost always: “How and why are we here?” If we’re lucky, we can just look at the source code and make some educated guesses. The most common method of improving this process is through stack traces. The ability to generate stack traces is the hallmark of a robust programming environment, but unfortunately this is also one feature often overlooked. Stack traces are often viewed as too difficult to construct, too expensive to make available in production, or simply not worth the effort to implement. Because they are commonly viewed as something that’s required only in exceptional circumstances, stack traces can often be expensive to calculate. As the complexity of a system grows and as asynchrony is employed to a larger extent, however, this view becomes less tenable. In a message-passing system, for example, the context in which the original message was enqueued is often more important than the context of the failure once the message has been dequeued. In an AJAX environment (where asynchronous was worthy of a spot in the acronym), the need for closures often makes the context in which they have been instantiated more useful than the closures themselves.
Sadly, JavaScript support for stack traces is sorely lacking. The browsers that do support stack traces make them available only via thrown exceptions, and most browsers don’t provide them at all. Stack traces are never available within global handlers such as window.onerror(), as the arguments are defined by a DOM that optimizes for the lowest common denominator. A window.onexception() handler that’s passed as an exception object would be a welcome addition.
Instead, we’re forced to catch all exceptions explicitly. On the surface, this seems like a daunting task—we don’t want to wrap every piece of code in a try/catch block. In an AJAX application, however, all JavaScript code is executed in one of four contexts:
˲Global context while loading scripts;
˲ From an event handler in response to user interaction;
˲ From a timeout or interval; or
˲ From a callback when processing an XMLHTTPRequest.
The first case we must defer to window.onerror(), but since it happens while scripts are loading, it would be hard for such bugs to escape development. For the remaining cases, we can automatically wrap callbacks in try/catch blocks through our own registration function as illustrated in Figure 1a.
The table here describes the information that is available from a global context and when catching particular types of exceptions for different browsers. The table demonstrates the limits of integrated browser support. Without reliable stack traces on every exception, we are forced to generate programmatic stack traces for better coverage. Thankfully, the semantics of the arguments object allows us to write a function to generate a programmatic stack trace as depicted in Figure 2.
A full implementation would provide a means for skipping uninteresting frames, including native stack traces (via a try/catch block), and providing a toString() method for converting the results. We don’t have file and line numbers, but we do have function names and arguments. Sadly, the proliferation of anonymous functions in JavaScript makes it difficult to get the canonical name of a function. The toString() method can give us the source for a particular function, but when printing a stack trace we need a name. The only effective way to accomplish this is to search the global namespace of all objects while constructing a human-readable name for the function along the way. This seems expensive, but we need to print the stack trace only in case of error. Most functions are either in the global namespace, one level deep, or two levels deep in the prototype of a particu-
References:
Archives