Saturday, February 23, 2013

Exception handling in JavaScript needs your love.

JavaScript exceptions cannot be captured selectively by way of subtype as is the convenient nature of C#. Instead one has to globally capture all potential exceptions and then make sense of them. If the exception you catch is not of the shape you hoped to filter for, then you will have to handle that what-do-I-do-with-this-thing-now problem with logic in your code. Captured exceptions unfortunately vary in mannerisms across different browser types, but the two properties that every JavaScript exception is going have are name and message:

function whatever() {
   try {
      freakout;
   }
   catch(error) {
      alert(error.name);
      alert(error.message);
   }
}

 
 

The alert for name above came back with "ReferenceError" for me in Firefox 16.0.2, Chrome 26.0.1410.12, and Safari 5.1.2 (7534.52.7) and "TypeError" in Internet Explorer 9.0.8112.16421 while the alert for message was "freakout is not defined" in Firefox and Chrome, "'freakout' is undefined" in Internet Explorer, and "can't find variable: freakout" in Safari. Clearly, the art of exception wrangling in JavaScript is not trivial. It is going to take some doing to make sense of errors and decide if they are something which you know you can handle within your application without stopping it (throw a modal to the screen with a message) or something you've not prepared for (let the application crash). You do need to take up the challenge however. Do not try to use an analysis tool like JSHint to find all possible errors for you or some other placebo to tell yourself that you write bug-free code. You're not that good. This was the subject of the first half of a talk by Lon Ingram of Waterfall Mobile (at AustinJS Tuesday night) which dovetailed into an overview of a testing tool he wrote called Reanimator which is named after the 1980s horror film. (When something crashes, Reanimator will simulate the nondeterministic user inputs, captured to a log, that led to the crash, on the other side of predefined initial state, in an attempt to reanimate the series of steps that led to the issue.)

How to catch: Mr. Ingram was not keen on catching exceptions from within a callback as he felt that any error messages one might get in callbacks were likely to be vague and thus worth little as they would not reveal the chain of events leading up to the callback. Instead, the actor who kicks off the asynchronous process which will hopefully fire off a callback is the better candidate for a try/catch block dress fitting. If a promise has a fail handler, then the fail handler could be referenced when throwing an error from a callback and, yes, this would be an exception to the rule just suggested. Also, window.onerror is the last line of defense for catching. Use it! For me, the variables captured in the example below behaved wildly different in all four of the browsers mentioned above. While url and lineNumber gave what you'd expect in Internet Explorer, Chrome, and Firefox, they didn't work in Safari where undefined was returned for url and 0 was returned for lineNumber. Again, only name and message are universal across all browsers. Internet Explorer gave the same thing it gave in the above example for message for error while Chrome, Firefox, and Safari instead gave a concatenation of name and message. Chrome’s was the strangest with "Uncaught ReferenceError: freakout is not defined" in contrast to Firefox's "ReferenceError: freakout is not defined" where the later browser didn't feel the need to append an extra word in advance of the beginning of the name/message concatenation. In Safari, I got: "ReferenceError: can't find variable: freakout"

window.onerror = function myErrorHandler(error, url, lineNumber) {
   alert(error);
   alert(url);
   alert(lineNumber);
}
function whatever() {
   freakout;
}

 
 

What to do with what you caught: You should probably send errors back to your own record keeping so that you'll know what your users and fighting out in the wild. When you build a database of this stuff, there will obviously be a lot of duplicates. You could try to "fingerprint" bugs to drop those which are not unique, but then again the duplicates and the metrics they offer may not necessarily be a bad thing. Suggested ways to send stuff back to yourself include:

  1. Largely invisible to the user, you could be serializing error messages and sending them back to yourself via AJAX. If you go this route you will want to get users to check a checkbox on a legal agreement given that you are collecting their information.
  2. Upon error, you could present a form to a user to complete to submit the error message back to home base. The form should be largely pre-populated with the data you wish to give to yourself, however this approach also allows users to type up their own commentary and hand it to you.
  3. You could give a user a mailto href link to click to send the data you want back to you by way of email. There may be a 2K cap on how much may be sent by a mailto link (depends on the browser) so this approach has some restraints. If you want to use a tool like stacktrace.js to capture the call stack from the point where you call an asynchronous function and then follow that up by handing yourself the serialized stack trace... this is NOT the best of the three options for you.

 
 

What to tell your users: It is best to communicate the errors of expected shapes back to the users. Tell the users what the bug was, how to report it, how much data was lost, and what they should do next. Should they just refresh the browser and try again?

No comments:

Post a Comment