Like any good ASP.Net developer I have Scott Guthrie’s blog bookmarked in my GReader. Recently I was browsing through the archives and came across an entry about not using Debug=”True” in production applications. I have to admit that I am VERY guilty of this since pretty much all of my applications are production and are using Debug=”True”. The reason for this is simple, I am just me and I have to monitor/update all of these applications. I do as much testing as I can before sending it off to production and moving on to the next project but often once the users start working with it something bad happens. The ability to have the errors logged including the line number in the source code is invaluable when going back to an application to fix a bug after a length of time. However, in light of Scott’s points it stops now.

Current Error Methods

First let me outline how I look after error handling currently and then I will discuss alternatives and changes to become Debug=”False” compliant. There are a few different components to my current method but all are very simple and chances are most of you already have a much better system.

Try/Catch Blocks

I only use a Try/Catch block when I know exactly what might happen and how to recover from it. In fact I use them really only in three instances: Transactions, Date Conversion Checking and LDAP Authentication. The reason for this is because these cases all require (as far as I know) that you catch exceptions in order to handle a normal failure case. Lets take a quick look why. Transactions are for atomic database handling and allow you to cancel all partial queries if anything during a block of code causes an exception. So you must catch any exception so that you can rollback the transaction and keep the databases referential integrity. I have never been able to find an exception free way (save by doing a lot of coding) to check if a date is in the proper format to be put into a date object thus a catch is needed. Finally, the method to check user credentials with LDAP is to try connecting to it using those credentials. When you connect with invalid credentials .Net’s LDAP module raises an exception which you must catch to provide login failed information. Every other case where an exception might occur I program my way out of it by defensive checking and failing that let the exception propagate beyond the Page.

Application Error Handler

ASP.Net provides you the ability to override the application wide error handler with your own code so that you can log any errors that propagate up from your pages. You do this in the global.asax file that sits in your application directory beside your web.config file. Here is mine:

<%@ Import Namespace="System.Web" %>
<%@ Import Namespace="System.IO" %>

<script runat="server">
  Sub Application_Error(ByVal sender As Object, ByVal e As EventArgs)
    Dim err As Exception = Server.GetLastError()
    Dim ctx As HttpContext = HttpContext.Current

    ' Collect useful data
    Dim user As String = ctx.Request.ServerVariables("AUTH_USER")
    Dim ip As String = ctx.Request.ServerVariables("REMOTE_ADDR")
    Dim filename As String = ctx.Request.PhysicalApplicationPath & "errors.txt"
    Dim sw As StreamWriter = File.AppendText(filename)
    sw.WriteLine("--------------------------------- ")
    sw.WriteLine("ERROR: [ " & Date.Now() & " ] [ " & user & " ] [ " & ip & " ]")
    sw.WriteLine("URL: " & ctx.Request.Url.AbsolutePath)
    sw.WriteLine("QUERYSTRING: " & ctx.Request.QueryString.ToString)
    sw.WriteLine("FORM: " & ctx.Request.Form.ToString)
    sw.WriteLine("REFERER: " & ctx.Request.ServerVariables("HTTP_REFERER"))

    Dim cnt As Integer = 0   ' Avoid infinite looping (just in case)
    err = err.InnerException
    While cnt < 5 And Not err Is Nothing
      If cnt = 0 Then
        sw.WriteLine("======== EXCEPTION ========")
      Else
        sw.WriteLine("----- INNER EXCEPTION -----")
      End If
      sw.WriteLine("MESSAGE: " & err.Message)
      sw.WriteLine("TYPE: " & err.GetType().ToString)
      sw.WriteLine("SOURCE: " & err.Source)
      sw.WriteLine("TARGETSITE: " & err.TargetSite.ToString)
      sw.WriteLine("STACKTRACE:" & vbNewLine & err.StackTrace)
      err = err.InnerException
      cnt += 1
    End While

    ' Finalize writing to file
    sw.WriteLine(vbNewLine)
    sw.Close()
  End Sub
</script>

The gist of the code is to grab the error that causes us to be in the error handler and the current HTTP context and we’ll dump as much information as possible into errors.txt for debugging. With Debug=”True” this includes the line number that caused the exception. At this point all that is needed is to read the exception and find the bug. You can of course email the exception to yourself instead of logging it to a text file if you wish.

No more Debug=”True”

Now let us discuss what to do in the absence of Debug=”True”. All the above will still work exactly as before except that the line number is missing. There is no solution for this since the debug symbols that gave you the line number in the first place are now gone, compiled code has no line numbers.

Try/Catch Blocks

You can extend the use of your exception handling to include an error wrapper that collects the exceptions from a block of code and wraps it in another exception which then will be logged. This way you know exactly which block of code caused the error. Try not to make your Try/Catch blocks too small because the overhead might cause performance degradation.

Re-Enable Debug=”True”

If you have an exception happen that you can’t track down using the information provided then you can temporarily re-enable debug mode until you get the error again with the line number. This isn’t a very good solution since you are still using Debug=”True” in production.

Draw it out

Drawing out the relationships between the code and data is my method of choice for solving bugs I can’t find quickly. Narrow down where the error came from as best you can within the offending Page. It is almost a guarantee that the error was causes by user input so first write down all the inputs to the block of code on the left side of the page. Take a look at the controls that collect the inputs and figure out what the user can possibly enter. Next you have to consider the malicious case of page hijacking (which circumvents MaxLengths, hidden value reliability, JavaScript checks and updates, etc.). Are you dropping these inputs directly into a database that only allows a certain number of characters? Will an empty string cause problems? Take each input one at a time and walk it through its code path keeping in mind the potential inputs you calculated. Somewhere along one of the paths of one of the inputs is your exception.

Error handling is a very important part of programming and hopefully these tips will help you to be able to find and fix your applications bugs in a faster and more efficient way. I would really appreciate your thoughts or improvements on these methods.