The Javascript Stacktrace Blog Post

For the past week or so I did some research on how best to include scripts in Tutti's sandbox environment and still get the best stacktraces possible. I got sucked in deeper and deeper and it just got uglier and uglier. This post will attempt to summarize all the knowledge that I've uncovered.

What's a Stacktrace?

When I say stacktrace, I mean it in the context an exception being thrown in your code, and getting the information about where exactly in your code the exception was thrown. This is what a exception might look like

Error: Buck stops here.
at failInlined (http://localhost:8000/error_object/:16:11)
at catchError (http://localhost:8000/util.js:77:26)
at http://localhost:8000/error_object/:49:1

First, there is an error message. Then, the stacktrace. The stacktrace basically represents the state of the program's call stack at the point when the exception was thrown. The stacktrace also contains the location(file URL and line number) of each call in the stack, which is extremely useful for debugging.

So What's the Problem?

The problem, is that not all browsers give you informative stacktraces. Not only that, different browsers give you the information in different ways. 

Most browsers today give you the stacktrace information in the Error object that was thrown. In Firefox, Chrome and Opera, for example, the Error object contains a stack property which is a string representation of the stacktrace, much like the example shown above.

// Get stacktrace on Firefox, Chrome and Opera
try{
   throw new Error('Buck stops here')
}catch(e){
   console.log(e.stack) // Yea!
}

In Safari, however, the stack property is non-existent: it does not give you the full stacktrace. Instead it gives you the location for the top stack frame only. This is gotten from the properties sourceURL and line.

// Get 1-level stacktrace on Safari
try{
  throw new Error('Buck stops here')
}catch(e){
  console.log(e.sourceURL + ': ' + e.line) // Meh
}

On IE, you are out of luck. There is no stacktrace information available on the Error object.

Going Deeper

Javascript can be executed in various different ways/techniques, and not all of them yield good stacktraces. My next objective was to see what the stacktraces look like for the different techniques

Technique 1: Script Tag Includes

The script tag include is probably the most common way to execute Javascript on a webpage.

<script src="myscript.js"></script>

Technique 2: Inlined Script Tag

Alternatively you can inlined Javascript code inside of the script tag.

<script>
console.log('Hello world')
</script>

Technique 3: Dynamically Inserted Script Tag

You can also create a script element dynamically in Javascript and load some Javascript into it.

var script = document.createElement('script')
if (<IE sucks and you are it>)
  script.text = jsCode
else
  script.appendChild(document.createTextNode(jsCode))
document.body.appendChild(script)
document.body.removeChild(script)

Technique 4: Eval

Use eval to execute the Javascript in Javascript. You probably want to global eval it.

// This code for global eval stolen from jQuery 1.6.1
(window.execScript || function(jsCode) {
	window["eval"].call(window, jsCode);
} )(data);

Technique 5: Data URL in the Script Tag

Using a data URL in a script tag is...I am not sure if anyone actually does this, and I am not entirely sure it's useful, but what the heck.

<script url="data:application/javascript,console.log('hello%20world')"></script>

Summary: Stacktrace Information From Error Object

Long story short, I tested the stacktrace output for each of these five techniques in different browsers. I summarized the results in this here table (that's right, geek out!)

Browser Include Inlined Dynamic Eval'ed Data URL
LN SR ST AG LN SR ST AG LN SR ST AG LN SR ST AG LN SR ST AG
IE 6 - 8 Unsupported
IE 9
Safari 5 Y Y Y Y Y* Y Y Y* Y
Chrome 11 Y Y Y Y Y Y Y* Y Y Y* Y Y Y* Y Y
Firefox 3.6 - 4 Y Y Y Y Y Y Y Y Y* Y Y Y Y* Y Y Y Y* Y Y Y
Opera 11 Y Y Y Y* Y Y Y* Y Y Y Y* Y Y
*
Line number may not be helpful
LN
Line numbers
SR
Source file URL
ST
Full stacktrace
AG
Call arguments in stacktrace
Include
Script tag with src set to a Javasrcipt file URL
Inlined
Script tag with inlined Javascript
Dynamic
Dynamically inserted script tag with inlined Javascript
Eval'ed
Global eval'ed Javascript
Data URL
Script tag with src set to a Data URL containing Javascript

A straightforward script tag include yields the best stacktrace information. As the asterisk(*) indicates, the dynamic script element, global eval, and data URL techniques can yield stacktraces with non-helpful line numbers. The data URL technique actually gives correct line numbers, it's just that all the newlines have been encoded and the entire source jammed onto a single line - so, I can't really say the line numbers are all that helpful. Strangly, Opera yields non-helpful line numbers even with the Javascript-inline-in-a-script tag technique. 

So That's it? No IE?

There's no way to get stack information on IE, not unless you want to do the other thing, but you don't want to know about that.

What's that? You do want to know? No you do not! Just forget I said anything.

Okay! I'll tell you, but you are going to regret it.

window.onerror

On IE - and as it happens on Firefox and Chrome as well - you can register an onerror handler on the window object, like so

window.onerror = function(message, fileURL, lineNumber){
  log(message + ': ' + fileURL + ': ' + lineNumber)
}

The handler will be called anytime a thrown exception is uncaught: propagates all the way to the bottom of the call stack with no catchers. Catching the exception will cause the handler not to be called, unless you rethrow it, but even then, there are other complications. When the handler is called, there is no error object, only the error message, file URL, and the line number.

Bad News: IE

Because of various IE bugs, the fileURL and line number passed into the onerror handler isn't always accurate or helpful. After some research, I've isolated these bugs:

  • In IE 7 and below, if an exception is thrown from within the code of an included script, fileURL will erroneously be set to the URL of the current HTML document even though the lineNumber will be set to the correct line number from within the script.
  • In IE 8 and below, if an exception is thrown using the throw statement, (i.e. throw new Error() ) and is caught, and then re-thrown, and then propagates all the way up, the line number reported will be the the place it was re-thrown, rather than the place it was first thrown. This bug does not surface when the error is triggered otherwise, such as with type and reference errors.

IE 9 has both of these bugs fixed, even when it emulates older versions of the browser.

What Can You Throw

The "throw" statement lets you throw just about any old value - even numbers and strings! But this is not recommended as you won't be able to get any line numbers or stacktraces if the value thrown isn't an object - except for in IE, where you can get line numbers using onerror. In IE, throwing anything other than a pre-defined error object(Error, TypeError, etc) will result in the loss of a meaningful message in both the caught error object and in the onerror handler - where it will just say "Exception thrown and not caught".

So, the simplest recommendation here is to only throw the pre-defined Error object when you want to explicitly throw an error using the "throw" statement.

throw new Error('Buck stops here.')

or better yet, don't use the throw statement at all(this avoids the second IE bug mentioned earlier).

The DIY Stacktrace

The DIY stacktrace was pioneered by Eric Wendelin among others. It first looks up argument.callee to get the current function being executed, then it walks the call stack by accessing the function's caller attribute recursively. For example:

> function f(){
    g()
  }
> function g(){
    h()
  }
> function h(){
    console.log(arguments.callee.caller.caller)
  }
> f()
function f(){
  g()
}

In the example, we are able to access the function f from with the body of h's execution by walking two levels up the call stack using the caller attribute.

Pros and Cons of the DIY Stacktrace

The advantage of the DIY stacktrace is that it will work in any browser.

The disadvantages are

  1. It will not get you file locations/line numbers.
  2. It does not work with thrown errors - either implicitly or explicitly - because the building of the stacktrace needs to be explicited done as its own statement, say, printStackTrace, for example. So, you cannot have a stacktrace and an error thrown - you cannot have your cake and eat it too.

Conclusion

Sorry I don't have a better conclusion than: It's a great big mess. However, with the knowledge covered here, I think it is possible to employ some sophisticated hackery to recover at least some stacktrace info for each browser - the hardest case being IE. Such a technique - if made practical - would be useful for unit test frameworks and crash reporting.

References

blog comments powered by Disqus