Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

User Friendly ASP.NET Exception Handling

0.00/5 (No votes)
20 Dec 2004 44  
A flexible framework for user-friendly ASP.NET exception handling, and automatically notifying developers of problems before users do.

Sample Image - ASPNETExceptionHandling.gif

Introduction

This article is a follow-up to my previous CodeProject article, User Friendly Exception Handling, which covered global exception handling for console and WinForms apps. As I mentioned in that article:

This leaves out one large class of .NET applications: web apps. I've experimented with this, and I do not believe it is desirable to handle web exceptions using the exact same classes that you use to handle Console and WinForms exceptions. They have different needs. I do have a variant of this class I use in server-side Web Services and ASP.NET applications, but it's significantly different.

This article covers a similar global exception handling technique for ASP.NET applications and Web Services. I won't go over any of the background on exceptions, as that was covered in the introduction to the previous article. Let's jump right into the implementation.

Unhandled Exceptions in ASP.NET Websites

There are two approaches you can use when you set out to design a global exception handler in ASP.NET.

The first method is the most straightforward: using the Application_Error event in the Global.asax file. This event fires whenever an unhandled exception occurs in an ASP.NET application. So, we can hook up our shared AspUnhandledException class there:

Sub Application_Error(ByVal sender As Object, ByVal e As EventArgs)
    Dim ueh As New ASPUnhandledException.Handler
    ueh.HandleException(Server.GetLastError.GetBaseException())
End Sub

Most ASP.NET developers are familiar with global.asax, so this method is easy to explain. It's also the method I used in the previous version of this article. While you can still do this, there is a better way.

And that second, better way is to hook into the ASP.NET HTTP pipeline by implementing IHttpModule:

Public Class UehHttpModule
    Implements IHttpModule
    Public Sub Init(ByVal Application As System.Web.HttpApplication) 
             Implements System.Web.IHttpModule.Init
        AddHandler Application.Error, AddressOf OnError
    End Sub
    Public Sub Dispose() Implements System.Web.IHttpModule.Dispose
    End Sub
    Protected Overridable Sub OnError(ByVal sender As Object, 
           ByVal args As EventArgs)
        Dim app As HttpApplication = CType(sender, HttpApplication)
        Dim ueh As New ASPUnhandledException.Handler
        ueh.Handle(app.Server.GetLastError)
    End Sub
End Class

This is functionally identical to the first method, with one key difference: you can now implement global exception handling without recompiling the ASP.NET application! This means you can retrofit unhandled exception handling to any ASP.NET website with ease -- just copy the ASPUnhandledException.dll file to the \bin folder, then make a minor edit to the Web.config:

<system.web>
  <httpModules>
  <add name="UehHttpModule"                 
       type="ASPUnhandledException.UehHttpModule, ASPUnhandledException" />  
  </httpModules>
</system.web>

And off you go. Isn't that cool?

Unhandled Exceptions in ASP.NET Web Services

Unfortunately, neither of these methods will work for a .NET Web Service. Application_Error will never fire, and the HTTP pipeline is bypassed in favor of the SOAP pipeline. To globally handle exceptions for a web service, we have to implement a SoapExtension:

Public Class UehSoapExtension
    Inherits SoapExtension
    
    Private _OldStream As Stream
    Private _NewStream As Stream
    
    Public Overloads Overrides Function GetInitializer( _
    ByVal serviceType As System.Type) As Object
        Return Nothing
    End Function
    
    Public Overloads Overrides Function GetInitializer( _
    ByVal methodInfo As System.Web.Services.Protocols.LogicalMethodInfo,  _
    ByVal attribute As System.Web.Services.Protocols.SoapExtensionAttribute) 
    As Object
        Return Nothing
    End Function
    
    Public Overrides Sub Initialize(ByVal initializer As Object)
    End Sub
    
    Public Overrides Function ChainStream(ByVal stream As Stream) As Stream
        _OldStream = stream
        _NewStream =  New MemoryStream 
        Return _NewStream 
    End Function
    
    Private Sub Copy(ByVal fromStream As Stream, ByVal toStream As Stream)
        Dim sr As New StreamReader(fromStream)
        Dim sw As New StreamWriter(toStream)
        sw.Write(sr.ReadToEnd())
        sw.Flush()
    End Sub
    
    Public Overrides Sub ProcessMessage(ByVal message _
    As System.Web.Services.Protocols.SoapMessage)
        Select Case message.Stage
            Case SoapMessageStage.BeforeDeserialize
                Copy(_OldStream, _NewStream)
                _NewStream.Position = 0
            Case SoapMessageStage.AfterSerialize
                If Not message.Exception Is Nothing Then
                    Dim ueh As New Handler
                    Dim strDetailNode As String
                    '-- handle our exception, and get the SOAP <detail> string

                    strDetailNode = ueh.HandleWebServiceException(message)
                    '-- read the entire SOAP message stream into a string

                    _NewStream.Position = 0                    
                    Dim tr As TextReader = New StreamReader(_NewStream)
                    '-- insert our exception details into the string

                    Dim s As String = tr.ReadToEnd
                    s = s.Replace("<detail />", strDetailNode)
                    '-- overwrite the stream with our modified string

                    _NewStream = New MemoryStream
                    Dim tw As TextWriter = New StreamWriter(_NewStream)
                    tw.Write(s)
                    tw.Flush()
                End If
                _NewStream.Position = 0
                Copy(_NewStream, _OldStream)
        End Select
    End Sub
    
End Class

The SoapExtension is a little more complicated than our HttpModule, because we have to modify the SOAP message to include the detailed exception information. I'll cover this in more detail later. All you need to do to get this working in your Web Service is copy the ASPUnhandledException.dll file to the \bin folder, then make a minor edit to the Web.config:

<webServices>
  <soapExtensionTypes>
    <add type="ASPUnhandledException.UehSoapExtension, ASPUnhandledException"
         priority="1" group="0" />
  </soapExtensionTypes>
</webServices>

Configuring AspUnhandledException

The AspUnhandledException class is configured through a custom configuration section named <UnhandledException> in Web.config. The class will work without any configuration, but all you'll get by default is a plain text log file in the root of your website. I suggest adding the following to your Web.config as a practical minimum:

<configSections>    
  <section name="UnhandledException" 
     type="System.Configuration.NameValueSectionHandler, System, 
           Version=1.0.5000.0, Culture=neutral, 
           PublicKeyToken=b77a5c561934e089" />
</configSections>

<UnhandledException>
  <add key="ContactInfo" value="Ima Testguy at 123-555-1212" />
  <add key="EmailTo" value="me@mydomain.com" />
  <add key="SmtpDefaultDomain" value="mydomain.com" />
  <add key="SmtpServer" value="mail.mydomain.com" />
</UnhandledException>

This is the complete list of configuration settings:

Key Default Description
EmailTo none semicolon delimited list of email addresses to send exception notifications to
EmailFrom server@domain.com override for the from address provided in exception emails. Optional.
IgnoreDebug True do not handle any exceptions when the debugger is attached, or if the originating exception host is localhost. If you want to debug the error handler, be sure to set this to False.
IgnoreRegex "" If this regular expression pattern evaluates to true against any part of the exception string, ignore this exception.
LogToEventLog False write an unhandled exception event to the Event Log
LogToEmail True send an unhandled exception email to the addresses in EmailTo
LogToFile True write an unhandled exception entry to a plain text log file
LogToUI True automatically display a customized unhandled exception HTML page
PathLogFile "" path to the plain text exception log file. Can be fully qualified or relative, with or without a filename. If it is relative, assumed to be relative to the root folder of the website. If no path provided, the default filename is used at the root of the website.
AppName none used on default error HTML page to replace any instances of "(app)" in strings
ContactInfo none used on default error HTML page to replace any instances of "(contact)" in strings
PageTitle (see screenshot) title of default error HTML page
PageHeader (see screenshot) header of default error HTML page
WhatHappened (see screenshot) Text to display in default error HTML web page under the "What happened:" heading
HowUserAffected (see screenshot) Text to display in default error HTML web page under the "How this will affect you:" heading
WhatUserCanDo (see screenshot) Text to display in default error HTML web page under the "What you can do about it:" heading

Once you've copied the .dll file to the /bin folder, and checked your Web.config settings, everything else is automatic from this point on.

ASPUnhandledException with ASP.NET websites

Okay, so now that you've hooked this up, what do you get for your troubles? Well, whenever an unhandled exception occurs in your website or web service, one or more of the following will automatically happen:

  1. A user friendly webpage is displayed to the user
  2. An email notification is sent to the address(es) of your choice
  3. A plaintext log file is written to the path and filename of your choice
  4. An entry is written to the event log

All of these options will contain the same detailed diagnostic information about the exception, which is generated by ExceptionToString. I'll now cover each of these events in more detail.

If LogToUI is left at the default of True, we automatically generate a user-friendly web page based on the GUI design laid out by Alan Cooper in his book About Face: The Essentials of User Interface Design, in the chapter titled The End of Errors. Note that if you turn off LogToUI, you'll revert to the default ASP.NET "yellow screen of death" page.

screenshot of custom unhandled exception web page

If LogToEmail is left at the default of True, your unhandled exceptions will automatically be emailed via the bundled managed SMTP class SimpleMail. This class requires valid SMTP settings in the Web.config custom configuration section <UnhandledException>. The complete list of configuration settings is:

Key Default Description
SmtpDefaultDomain none Default email domain name used for emails that don't provide a domain
SmtpServer none SMTP email server used to send emails
SmtpPort 25 SMTP port used to send emails
SmtpAuthUser none If outgoing mail authentication is enabled, user name used for authentication
SmtpAuthPassword none If outgoing mail authentication is enabled, password used for authentication

The exception email contains:

  • User and machine identification
  • Application information
  • Exception summary
  • Custom stack trace
  • All ASP.NET collection contents

screenshot of automatic exception email

If LogToFile option is left at the default of True, unhandled exceptions will be written to a plain text log file in the root of your website, named UnhandledExceptionLog.txt. If you require a different path, use the PathLogFile setting to specify either a relative or absolute path, with or without a filename. If the path is relative, it is assumed to be relative to the root of the current website. Whatever path you specify must have ASP.NET write permissions, otherwise the logging will silently fail.

If LogToEventLog is enabled, you will get this same detailed exception information in the Application Event Log. Note that you must grant the ASP.NET process account permission to write to the registry for this to function. I don't frequently do this in my apps, which is why I defaulted it to False, but it does work!

ASPUnhandledException with ASP.NET Web Services

In my previous article, I bemoaned the lack of good global error handling provisions for ASP.NET Web Services. Well, not any more. Using a custom SoapExtension, it's easy! And generally, it works the same as in ASP.NET. There are, however, a few important differences you should be aware of:

  1. Using a SOAPExtension means we only catch SOAP unhandled exceptions-- you will not be able to test unhandled exceptions using the web browser interface! Please bear this in mind! I included a demo SOAP console application in the solution for this very reason.
  2. SOAP clients don't have a browser interface, so the LogToUI option is not available for Web Services, and is ignored in this case.
  3. The SoapException is a bit different than your typical .NET Exception, it contains a special XML <detail> element for details on the server part of the exception.

The only tricky part left is inserting the rich exception information we've come to expect from our ASP.NET exceptions into the SOAP message. By default, the <detail> element is very sparse, so almost nothing is communicated from the server to the client:

<soap:Fault>
  <faultcode>soap:Server</faultcode>
  <faultstring>SoapException</faultstring>
  <detail/>
</soap:Fault>

Hard to diagnose this exception with so little information. That's where the Handler.HandleWebServiceException comes in. We call this method from the SoapExtension, and it generates a much better <detail> element for us:

Public Function HandleWebServiceException(ByVal sm As _
              System.Web.Services.Protocols.SoapMessage) As String
    _blnLogToUI = False
    HandleException(sm.Exception)
    
    Dim doc As New Xml.XmlDocument
    Dim DetailNode As Xml.XmlNode = doc.CreateNode(XmlNodeType.Element, _
        SoapException.DetailElementName.Name, _
        SoapException.DetailElementName.Namespace)
    
    Dim TypeNode As Xml.XmlNode = doc.CreateNode(XmlNodeType.Element, _
        "ExceptionType", _
        SoapException.DetailElementName.Namespace)
    TypeNode.InnerText = _strExceptionType
    DetailNode.AppendChild(TypeNode)
    
    Dim MessageNode As Xml.XmlNode = doc.CreateNode(XmlNodeType.Element, _
        "ExceptionMessage", _
        SoapException.DetailElementName.Namespace)
    MessageNode.InnerText = sm.Exception.Message
    DetailNode.AppendChild(MessageNode)
    
    Dim InfoNode As Xml.XmlNode = doc.CreateNode(XmlNodeType.Element, _
        "ExceptionInfo", _
        SoapException.DetailElementName.Namespace)
    InfoNode.InnerText = _strException
    DetailNode.AppendChild(InfoNode)
    
    Return DetailNode.OuterXml.ToString()
End Function

The string returned from this method is used to modify the SOAP message "in flight", and we get a much better <detail> node:

<soap:Fault>
  <faultcode>soap:Server</faultcode>
  <faultstring>Server was unable to process request. -->

I trimmed many of the <ExceptionInfo> lines, but it's the same rich diagnostic information you've seen before.

Conclusion

One final observation on Unhandled Exceptions, before we close our discussion. You must avoid exceptions in the Unhandled Exception Handler. This is a special case handler, the "handler of last resort", and exceptions in this code are extremely bad form. They can cause the code to terminate with no warning whatsoever, as if you put in an arbitrary Return right smack dab in the middle of your function. I have taken great precautions in my handler to avoid exceptions, but be warned.

I've used this class on about a dozen different websites and web services so far, with excellent results. There are many more details and comments in the source code provided at the top of the article, so check it out. Please don't hesitate to provide feedback, good or bad!

I hope you enjoyed this article. If you did, you may also like my other articles as well.

History

  • Saturday, August 21, 2004 - Published
  • Monday, September 27, 2004 - Updated demo code
    • Fixed problem where inner exceptions were discarded in global.asax.
    • Added code to ignore the outermost ASP.NET exception (it's the same every time).
    • Removed ConfigurationException that was previously thrown when Company and Product were not populated in AssemblyInfo.
  • Sunday, October 17, 2004 - Version 2.0
    • Added SoapExtension and HttpHandler.
    • Many improvements to base Handler class (and a few bug fixes).
    • Rebuilt article and demo solution.
  • Saturday, December 18, 2004 - Version 2.1
    • Uses a separate custom <UnhandledException> .config file section.
    • added .config values EmailFrom, PageTitle and PageHeader.
    • viewstate is sent as a plaintext email attachment (when present).
    • smarter, more concise formatting of ASP.NET collections.
    • added ASP.NET Session, Cache, and Application collection summary.
    • more feedback provided in the LogToUi page when there is a failure to log via email, file, or event log.
    • the "More Details:" section of the HTML error page is now an expander button to simplify the output for casual users.
    • converted to VB.NET 2005 style XML comments.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here