Dealing with Support Costs - Part 2 (Posted by Avi)

It's been a while since any of us posted! A bunch of factors have contributed to this, including paternal leave, the fiscal year rollover (we own a budgeting tool, so it's always fun around June/July), and several high-priority projects.

I'll try to continue my "Dealing with support costs" series. Logically, I should move onto design and code reviews, but I don't feel like it. Instead talk about exception handling… Imagine this scenario:

  1. User clicks on a button.
  2. User gets error message: "Sorry, we've encountered an error. We'll contact you shortly."
  3. <2 minutes go by…>
  4. User gets an email saying: "Hey, user, we noticed you just got an error while doing X. We fixed it for you, have a nice day!".

That's how we've got our apps working. To the user, this can sometimes seem like a miracle ("How did you know?!"), but of course it's pretty simple. All unhandled exceptions in our apps get funneled through our common exception handler (which lives in a shared assembly in the GAC). That handler sends us an email with the stack trace (and some other info, such as username, machine name, etc), and writes an eventlog entry.

We have inbox rules to catch these emails and play a loud annoying sound when we get them. And when we do… We pounce on the issue as soon as we can.

At the start, we got a lot of these. But you quickly learn the value of checking every input field when you see a few "Input string was not in a correct format"s :)

One obvious way of using this is by explicitly catching unexpected errors using a try/catch. But it has much more effect if you also add it to the global.asax, that way even a bug in the code where the programmer failed to catch an exception will still be caught. Here's what it looks like:

void Application_Error(Object sender, EventArgs e)
{
  // We store some settings in the config file as to whether to publish errors, where to send
  // them to, what app name to use, etc.
if (bool.Parse(ConfigurationSettings.AppSettings["PublishErrors"]))
{
Exception ex = Server.GetLastError();
if (ex.GetType() == typeof(System.Web.HttpException))
{
System.Web.HttpException httpEx = (System.Web.HttpException)ex;
int httpErrorCode = httpEx.GetHttpCode();

      // Don't publish 404s. They're usually just users mistyping a URL.
if (httpErrorCode == 404)
{
HttpContext.Current.Items.Add("ErrorMessage", "Http 404. The resource you are looking for (or one of its dependencies) could have been removed, had its name changed, or is temporarily unavailable. Please review the URL and make sure that it is spelled correctly.");
Server.Transfer("~/Error.aspx");
return;
}
}

    HttpContext.Current.Items.Add("ErrorMessage", "Oops!. An unexpected error has occured. The Webmasters have been notified. Please contact <a href='mailto:dditweb'>dditweb</a> if you need immediate assistance.");

    // Publish all other exceptions.
    // This method sends mail, writes an eventlog entry, etc.
DDRTLib.ExceptionHandler.Publish(ex);
Server.Transfer("~/Error.aspx");
}
}

Overall, while this doesn't reduce the number of errors, it does at least this:

  • Shows the user a friendlier error message.
  • Lets us know how much trouble users are having with our apps (including non-user errors such as timeouts).
  • Allows us to solve small UI issues before they get out of hand.

Avi