Handling JavaScript Exceptions

I have previously apologized for not being the most sophisticated JavaScript programmer on the block.  But one of the advantages of learning while I blog is that occasionally I discover things that a smart JavaScript programmer would already know, but is interesting to people who are climbing the same learning curve as me.

Today I learned more about JavaScript exceptions.  Or more specifically, the JavaScript exceptions thrown by script errors in XFA.

I’ve written lots of code that handles exceptions this way:

try
{
    xfa.template.foo = "bar";
} catch (err)
{
    app.alert(err.toString());
}

As it turns out, this is not a very satisfying way to report exceptions.  The error object that gets thrown by the script engine does not provide helpful information from the toString() method.  Typically it returns something like: "GeneralError: Operation failed.".  Thanks a lot.

Then it dawned on me that just maybe there was useful information locked up inside the exception object that the toString() method does not report.  I wrote some code (ok, I shamelessly copied some code from O’Reilly’s: JavaScript the Definitive Guide) to examine the contents of the object in my catch block.

I dumped the results out to a field on the form:

try
{
    doInit();
} catch(err)
{
    var vDebug = "";
    for (var prop in err)
    { 
       vDebug += "property: "+ prop+ " value: ["+ err[prop]+ "]\n";
    }
    vDebug += "toString(): " + " value: [" + err.toString() + "]";
    status.rawValue = vDebug;
}

function doInit()
{
    foo.ForceError();
}

The results of this dump show up as:

property: name value: [GeneralError]
property: message value: [Operation failed.]
property: fileName value: [XFA:form1[0]:page1[0]:Subform1[0]:initialize]
property: lineNumber value: [7]
property: extMessage value: [GeneralError: Operation failed.
XFAObject.bar:7:XFA:form1[0]:page1[0]:Subform1[0]:initialize
Invalid property set operation; template doesn’t have property ‘bar’

]
property: number value: [14]
property: stack value: [Error()@:0
nestedFunction()@2:7
ForceError()@2:3
ForceError()@:0
doInit()@XFA:form1[0]:page1[0]:Subform1[0]:initialize:17
@XFA:form1[0]:page1[0]:Subform1[0]:initialize:3
]
toString():  value: [GeneralError: Operation failed.]

Turns out there’s lots of great information available.  The exception we catch comes populated with properties: name, message, filename, lineNumber, extMessage, number and stack.  The toString() method on this object ignores the most useful information and simply concatenates the name and message properties.  The extMessage property is what normally gets dumped to the JavaScript console.

With this knowledge, I can write an exception object formatter that turns the exception message into something both useful and human readable:

Invalid property set operation; template doesn’t have property ‘bar’
Stack:
line: 7 of nestedFunction()
line: 3 of ForceError()
line: 10 of doInit() in the initialize event of: page1.Subform2
line: 3 in the initialize event of: page1.Subform2

The code that produces that output can be found in the attached sample.

Server Side

You might not be aware that our JavaScript processor on the server is different from the one embedded in Acrobat/Reader.  Fortunately, in spite of being different processors, the compatibility between the two is very good.  However the exception object that gets thrown on the server has a different set of properties from the exception object thrown on the client.  The function I wrote to format an exception has conditional code that produces a different result for the server, based on the properties available there:

Invalid property set operation; template doesn’t have property ‘bar’
at line: 8 of script:
1:
2: function ForceError()
3: {
4:     nestedFunction();
5: }
6: function nestedFunction()
7: {
8:     xfa.template.bar = "bar";
9: }
10:
11: this.ForceError = ForceError; this.nestedFunction = nestedFunction;

Why Bother?

If you’ve been debugging JavaScript for some time, then you might wonder why I went to all this trouble, since most of the useful information from a JavaScript error is dumped to the console anyway.  Three reasons:

  1. Some form authors make a practise of wrapping all their JavaScript code in try/catch blocks.
  2. The stack trace information is not part of the information dumped in the JavaScript console
  3. I found a bug where under certain conditions, JavaScript exceptions are *not* reported in the console.

The bug happens when an initialization script creates an instance of a subform.  If that new subform also has an initialization script, its errors go unreported in the console.  This condition (a nested initialization) is probably a bit unlikely for most forms, but has happened to me at least a couple of times.  My workaround is to wrap the initialization script in a try block and dump exception information to the console in the catch block.  And now that I have better tools for dumping exceptions I’m a happier form developer.

The Deep End

Server Side Debugging

When writing code for forms destined for the server, it is important to be able to dump information from your scripts so you can debug them.  In Reader/Acrobat using console.println() is invaluable.  But the console object is specific to Acrobat.  We need something for the server as well.  I use xfa.log.message().

A script to dump an error message then looks like:


catch (err)
{
    var vMsg = scMessage.prettyMessage(err) ;

    // client code
    if (xfa.host.name == "Acrobat")
        console.println(vMsg);

    // LiveCycle server
    if (xfa.host.name == "XFAPresentationAgent")
 &#16
0;      xfa.log.message(0, vMsg);

}

Modified Script

You might have noticed up above that the dump of the exception for the server had a line of code that did not appear in the form:

this.ForceError = ForceError; this.nestedFunction = nestedFunction;

This line gets added to the script by the XFA processor so that for a named script object we can support the syntax: foo.ForceError() and foo.nestedFunction().

Other Kinds of Exceptions

If you write JavaScript code that throws an exception, then the exception object will be whatever you throw.  For example you could do this:

throw "Bad date format";

In this case, the object caught in a catch block will be a string — not the exception object that XFA scripts throw.

March 19 Update

After using the sample code for a few days, I added a couple of improvements to the sample:

It turns out that the contents of the exception in Reader varies depending on the nature of the exception.  The code I wrote handled exceptions thrown by the XFA engine.  Regular JavaScript language exceptions did not populate an "extMessage" member of the exception class.  I updated the pretty printing of the exception accordingly, and added an example of this kind of exception to the form.

Since most of the time we want to display exceptions in the console or server log file, I added a handleError() method that does just that.  In addition, to logging to the console, it calls console.show() in order to make sure the form author sees the exception when they’re debugging.

August 14 Update

Improved the handling of message emitted in Designer.  Added support for thrown string objects.  These changes are explained here.  Download this fragment for the latest version of this script.