What Went Wrong with my Javascript?

Now that you’ve launched your new killer Web 2.0 website, how do you detect errors in your deployed Javascript?

Using onerror

The standard approach is to hook into the Window object’s onerror event handler and use Ajax to log your requests to the server. There are some good tutorials on the web on how to do this, but here is one approach:

// Register global error handler
window.onerror = function(message, uri, line)
{
  var fullMessage = message + "\n at " + uri + ": " + line
  remoteLogger.log(fullMessage)
  // Let the browser take it from here
  return false                      
}

Notice that you have to explicity set window.onerror using old-style event handlers, attachEvent and addEventListener don’t seem to do the trick. Also, Internet Explorer always stupidly sets the URI parameter to the URI of the current page, not the file that caused the problem. Thus you have to hunt through all the linked Javascript files looking at the specified line number and make a best guess as to which file caused the problem.

Getting a Stack Trace

Often times knowing where an error occurred is not enough – what you really need is the full stack trace. If your user has a Gecko based browser (eg, Firefox), you’re in luck.

Notice the stack parameter in the code above? Gecko has a poorly documented stack property on the global Error object that provides just what we need.

Let’s say you have code that looks like this:

function first()
{
  second()
}

second = function()
{
  blowUp()
}

function blowUp()
{
  try
  {
    foo.bar()
  }
  catch (e)
  {
    handleException(e)
  }         
}

If you call the first method, say from onload like in this example, the resulting stack trace is:

 blowUp()@file:///C:/temp/error.js:17
                ()@file:///C:/temp/error.js:10 
	   first()@file:///C:/temp/error.js:
	onload([object Event])@file:///C:/temp/error.html:1 
 	          @:0

Each line has the following format:

<method_name>(<arguments>)@<uri>:<line_number>

Notice that the method_name will be blank for anonymous functions, which are quite common if you follow the prototype javascript style.

A Simple Logger

Now let’s take a look at some code. First, here is a simple logger that posts errors to a server via Ajax (note this code assumes your are using the prototype library):

RemoteLogger = function RemoteLogger()
{
  this.logging = false
}

Object.extend(RemoteLogger.prototype,
{
  log: function(message, stack)
  {
    if (!this.logging)
    {
      this.logging = true

      var parameters = 'resource=error'
      var postBody = 'error=' + encodeURIComponent(message) + 
                     '&stack=' + encodeURIComponent(stack)

      var requestOptions = 
      {
        method: 'post',
        parameters: parameters,
        postBody: postBody,
        onSuccess: function(transport)
        {
          this.logging = false
        }.bind(this)
      }
    
      new Ajax.Request('/logger', requestOptions)
    }
  }
})

Handling Exceptions

Next, let’s define the handleException method we used above. This method extracts out useful information from an Error object and uses the logger above to post the results to a server.

function handleException(exception)
{  
  /* In FF exception can be a string if it happens
     when opening the xmlHttpRequest.  Gah! */
  if (typeof exception == 'string')
    exception = new Error(exception)
  
  /* If a xmlhttp request is happening in Mozilla and
     the user navigates to another page, then when
     the first request returns a NS_ERROR_NOT_AVAILABLE
     error will be thrown.  So just ignore it. */ 
  if (exception.name == 'NS_ERROR_NOT_AVAILABLE') return
      
  var fullMessage = ''
  var uri = ''
  var stack = ''
  var line = ''
      
  try
  {                    
    /* Don't use exception.toString since the JS spec
       does not require it to provide the error name or message
       (haven't tested to see if it matters though across browsers) */
    fullMessage = exception.name + ': ' + exception.message
    
    uri = exception.fileName
    stack = exception.stack
    
    // Firefox sometimes blows up here
    line = exception.lineNumber
  }
  catch (e)
  {
  }                  

  fullMessage += "\n at " + uri + ": " + line
  console.info(fullMessage)
  console.info(stack)
  remoteLogger.log(fullMessage, stack)      
}

Annoying Gotchas

There are few annoying gotchas to know about:

  • Stack traces are only available from Error objects thrown by exceptions and thus are not available from the onerror method.
  • Uncaught exceptions are not hanlded by onerror.
  • There is no global exception handler in Javascript, so you have to be very careful in the way you right your code. On the positive side, its easy to implement a global error handler for methods that are invoked due to the results of an Ajax request. On the other hand, its much harder to do this for methods invoked due to normal events generated by a user interacting with the browser.

Anyway, the more information you can get about browser errors the better – you’ll often be surprised by the results!

Leave a Reply

Your email address will not be published.

Top