Processing Unhandled Exceptions (VB)
View or download sample code (how to download)
When a runtime error occurs on a web application in production it is important to notify a developer and to log the error so that it may be diagnosed at a later point in time. This tutorial provides an overview of how ASP.NET processes runtime errors and looks at one way to have custom code execute whenever an unhandled exception bubbles up to the ASP.NET runtime.
Introduction
When an unhandled exception occurs in an ASP.NET application, it bubbles up to the ASP.NET runtime, which raises the Error
event and displays the appropriate error page. There are three different types of error pages: the Runtime Error Yellow Screen of Death (YSOD); the Exception Details YSOD; and custom error pages. In the preceding tutorial we configured the application to use a custom error page for remote users and the Exception Details YSOD for users visiting locally.
Using a human-friendly custom error page that matches the look and feel of the site is preferred to the default Runtime Error YSOD, but displaying a custom error page is only one part of a comprehensive error handling solution. When an error occurs in an application in production, it is important that the developers are notified of the error so that they can unearth the cause of the exception and address it. Furthermore, the error's details should be logged so that the error can be examined and diagnosed at a later point in time.
This tutorial shows how to access the details of an unhandled exception so that they can be logged and a developer notified. The two tutorials following this one explore error logging libraries that, after a bit of configuration, will automatically notify developers of runtime errors and log their details.
Note
The information examined in this tutorial is most useful if you need to process unhandled exceptions in some unique or customized manner. In cases where you only need to log the exception and notify a developer, using an error logging library is the way to go. The next two tutorials provide an overview of two such libraries.
Executing Code When TheError
Event Is Raised
Events provide an object a mechanism for signaling that something interesting has occurred, and for another object to execute code in response. As an ASP.NET developer you are accustomed to thinking in terms of events. If you want to run some code when the visitor clicks a particular Button, you create an event handler for that Button's Click
event and put your code there. Given that the ASP.NET runtime raises its Error
event whenever an unhandled exception occurs, it follows that the code for logging the error's details would go in an event handler. But how do you create an event handler for the Error
event?
The Error
event is one of many events in the HttpApplication
class that are raised at certain stages in the HTTP pipeline during the lifetime of a request. For example, the HttpApplication
class's BeginRequest
event is raised at the start of every request; its AuthenticateRequest
event is raised when a security module has identified the requestor. These HttpApplication
events give the page developer a means to execute custom logic at the various points in the lifetime of a request.
Event handlers for the HttpApplication
events can be placed in a special file named Global.asax
. To create this file in your website, add a new item to the root of your website using the Global Application Class template with the name Global.asax
.
Figure 1: Add Global.asax
To Your Web Application
(Click to view full-size image)
The contents and structure of the Global.asax
file created by Visual Studio differ slightly based on whether you are using a Web Application Project (WAP) or Web Site Project (WSP). With a WAP, the Global.asax
is implemented as two separate files - Global.asax
and Global.asax.vb
. The Global.asax
file contains nothing but an @Application
directive that references the .vb
file; the event handlers of interest are defined in the Global.asax.vb
file. For WSPs, only a single file is created, Global.asax
, and the event handlers are defined in a <script runat="server">
block.
The Global.asax
file created in a WAP by Visual Studio's Global Application Class template includes event handlers named Application_BeginRequest
, Application_AuthenticateRequest
, and Application_Error
, which are event handlers for the HttpApplication
events BeginRequest
, AuthenticateRequest
, and Error
, respectively. There are also event handlers named Application_Start
, Session_Start
, Application_End
, and Session_End
, which are event handlers that fire when the web application starts, when a new session starts, when the application ends, and when a session ends, respectively. The Global.asax
file created in a WSP by Visual Studio contains just the Application_Error
, Application_Start
, Session_Start
, Application_End
, and Session_End
event handlers.
Note
When deploying the ASP.NET application you need to copy the Global.asax
file to the production environment. The Global.asax.vb
file, which is created in the WAP, does not need to be copied to production because this code is compiled into the project's assembly.
The event handlers created by Visual Studio's Global Application Class template are not exhaustive. You can add an event handler for any HttpApplication
event by naming the event handler Application_EventName
. For example, you could add the following code to the Global.asax
file to create an event handler for the AuthorizeRequest
event:
Sub Application_AuthorizeRequest(ByVal sender As Object, ByVal e As EventArgs)
' Event handler code
End Sub
Likewise, you can remove any event handlers created by the Global Application Class template that are not needed. For this tutorial we only require an event handler for the Error
event; feel free to remove the other event handlers from the Global.asax
file.
Note
HTTP Modules offer another way to define event handlers for HttpApplication
events. HTTP Modules are created as a class file that can be placed directly within the web application project or separated out into a separate class library. Because they can be separated out into a class library, HTTP Modules offer a more flexible and reusable model for creating HttpApplication
event handlers. Whereas the Global.asax
file is specific to the web application where it resides, HTTP Modules can be compiled into assemblies, at which point adding the HTTP Module to a website is as simple as dropping the assembly in the Bin
folder and registering the Module in Web.config
. This tutorial does not look at creating and using HTTP Modules, but the two error logging libraries used in the following two tutorials are implemented as HTTP Modules. For more background on the benefits of HTTP Modules refer to Using HTTP Modules and Handlers to Create Pluggable ASP.NET Components.
Retrieving Information About the Unhandled Exception
At this point we have a Global.asax file with an Application_Error
event handler. When this event handler executes we need to notify a developer of the error and log its details. To accomplish these tasks we first need to determine the details of the exception that was raised. Use the Server object's GetLastError
method to retrieve details of the unhandled exception that caused the Error
event to fire.
Sub Application_Error(ByVal sender As Object, ByVal e As EventArgs)
' Get the error details
Dim lastErrorWrapper As HttpException = _
CType(Server.GetLastError(), HttpException)
End Sub
The GetLastError
method returns an object of type Exception
, which is the base type for all exceptions in the .NET Framework. However, in the code above I am casting the Exception object returned by GetLastError
into an HttpException
object. If the Error
event is being fired because an exception was thrown during the processing of an ASP.NET resource then the exception that was thrown is wrapped within an HttpException
. To get the actual exception that precipitated the Error event use the InnerException
property. If the Error
event was raised because of an HTTP-based exception, such as a request for a non-existent page, an HttpException
is thrown, but it does not have an inner exception.
The following code uses the GetLastErrormessage
to retrieve information about the exception that triggered the Error
event, storing the HttpException
in a variable named lastErrorWrapper
. It then stores the type, message, and stack trace of the originating exception in three string variables, checking to see if the lastErrorWrapper
is the actual exception that triggered the Error
event (in the case of HTTP-based exceptions) or if it's merely a wrapper for an exception that was thrown while processing the request.
Sub Application_Error(ByVal sender As Object, ByVal e As EventArgs)
' Get the error details
Dim lastErrorWrapper As HttpException = _
CType(Server.GetLastError(), HttpException)
Dim lastError As Exception = lastErrorWrapper
If lastErrorWrapper.InnerException IsNot Nothing Then
lastError = lastErrorWrapper.InnerException
End If
Dim lastErrorTypeName As String = lastError.GetType().ToString()
Dim lastErrorMessage As String = lastError.Message
Dim lastErrorStackTrace As String = lastError.StackTrace
End Sub
At this point you have all the information you need to write code that will log the exception's details to a database table. You could create a database table with columns for each of the error details of interest - the type, the message, the stack trace, and so on - along with other useful pieces of information, such as the URL of the requested page and the name of the currently logged on user. In the Application_Error
event handler you would then connect to the database and insert a record into the table. Likewise, you could add code to alert a developer of the error via email.
The error logging libraries examined in the next two tutorials provide such functionality out of the box, so there's no need to build this error logging and notification yourself. However, to illustrate that the Error
event is being raised and that the Application_Error
event handler can be used to log error details and notify a developer, let's add code that notifies a developer when an error occurs.
Notifying a Developer When an Unhandled Exception Occurs
When an unhandled exception occurs in the production environment it is important to alert the development team so that they can assess the error and determine what actions need to be taken. For example, if there is an error in connecting to the database then you'll need to double check your connection string and, perhaps, open a support ticket with your web hosting company. If the exception occurred because of a programming error, additional code or validation logic may need to be added to prevent such errors in the future.
The .NET Framework classes in the System.Net.Mail
namespace make it easy to send an email. The MailMessage
class represents an email message and has properties like To
, From
, Subject
, Body
, and Attachments
. The SmtpClass
is used to send a MailMessage
object using a specified SMTP server; the SMTP server settings can be specified programmatically or declaratively in the <system.net>
element in the Web.config file
. For more information on sending email messages in an ASP.NET application check out my article, Sending Email from an ASP.NET Web Pages Site, and System.Net.Mail.
Note
The <system.net>
element contains the SMTP server settings used by the SmtpClient
class when sending an email. Your web hosting company likely has an SMTP server that you can use to send email from your application. Consult your web host's support section for information on the SMTP server settings you should use in your web application.
Add the following code to the Application_Error
event handler to send a developer an email when an error occurs:
Sub Application_Error(ByVal sender As Object, ByVal e As EventArgs)
' Get the error details
Dim lastErrorWrapper As HttpException = _
CType(Server.GetLastError(), HttpException)
Dim lastError As Exception = lastErrorWrapper
If lastErrorWrapper.InnerException IsNot Nothing Then
lastError = lastErrorWrapper.InnerException
End If
Dim lastErrorTypeName As String = lastError.GetType().ToString()
Dim lastErrorMessage As String = lastError.Message
Dim lastErrorStackTrace As String = lastError.StackTrace
Const ToAddress As String = "support@example.com"
Const FromAddress As String = "support@example.com"
Const Subject As String = "An Error Has Occurred!"
' Create the MailMessage object
Dim mm As New MailMessage(FromAddress, ToAddress)
mm.Subject = Subject
mm.IsBodyHtml = True
mm.Priority = MailPriority.High
mm.Body = string.Format( _
"<html>" & vbCrLf & _
" <body>" & vbCrLf & _
" <h1>An Error Has Occurred!</h1>" & vbCrLf & _
" <table cellpadding=""5"" cellspacing=""0"" border=""1"">" & vbCrLf & _
" <tr>" & vbCrLf & _
" <tdtext-align: right;font-weight: bold"">URL:</td>" & vbCrLf & _
" <td>{0}</td>" & vbCrLf & _
" </tr>" & vbCrLf & _
" <tr>" & vbCrLf & _
" <tdtext-align: right;font-weight: bold"">User:</td>" & vbCrLf & _
" <td>{1}</td>" & vbCrLf & _
" </tr>" & vbCrLf & _
" <tr>" & vbCrLf & _
" <tdtext-align: right;font-weight: bold"">Exception Type:</td>" & vbCrLf & _
" <td>{2}</td>" & vbCrLf & _
" </tr>" & vbCrLf & _
" <tr>" & vbCrLf & _
" <tdtext-align: right;font-weight: bold"">Message:</td>" & vbCrLf & _
" <td>{3}</td>" & vbCrLf & _
" </tr>" & vbCrLf & _
" <tr>" & vbCrLf & _
" <tdtext-align: right;font-weight: bold"">Stack Trace:</td>" & vbCrLf & _
" <td>{4}</td>" & vbCrLf & _
" </tr> " & vbCrLf & _
" </table>" & vbCrLf & _
" </body>" & vbCrLf & _
"</html>", _
Request.RawUrl, _
User.Identity.Name, _
lastErrorTypeName, _
lastErrorMessage, _
lastErrorStackTrace.Replace(Environment.NewLine, "<br />"))
'Attach the Yellow Screen of Death for this error
Dim YSODmarkup As String = lastErrorWrapper.GetHtmlErrorMessage()
If Not String.IsNullOrEmpty(YSODmarkup) Then
Dim YSOD As Attachment = _
Attachment.CreateAttachmentFromString(YSODmarkup, "YSOD.htm")
mm.Attachments.Add(YSOD)
End If
' Send the email
Dim smtp As New SmtpClient()
smtp.Send(mm)
End Sub
While the above code is quite lengthy, the bulk of it creates the HTML that appears in the email sent to the developer. The code starts by referencing the HttpException
returned by the GetLastError
method (lastErrorWrapper
). The actual exception that was raised by the request is retrieved via lastErrorWrapper.InnerException
and is assigned to the variable lastError
. The type, message, and stack trace information is retrieved from lastError
and stored in three string variables.
Next, a MailMessage
object named mm
is created. The email body is HTML formatted and displays the URL of the requested page, the name of the currently logged on user, and information about the exception (the type, message, and stack trace). One of the cool things about the HttpException
class is that you can generate the HTML used to create the Exception Details Yellow Screen of Death (YSOD) by calling the GetHtmlErrorMessage method. This method is used here to retrieve the Exception Details YSOD markup and add it to the email as an attachment. One word of caution: if the exception that triggered the Error
event was an HTTP-based exception (such as a request for a non-existent page) then the GetHtmlErrorMessage
method will return null
.
The final step is to send the MailMessage
. This is done by creating a new SmtpClient
method and calling its Send
method.
Note
Before using this code in your web application you'll want to change the values in the ToAddress
and FromAddress
constants from support@example.com to whatever email address the error notification email should be sent to and originate from. You'll also need to specify SMTP server settings in the <system.net>
section in Web.config
. Consult your web host provider to determine the SMTP server settings to use.
With this code in place anytime there's an error the developer is sent an email message that summarizes the error and includes the YSOD. In the preceding tutorial we demonstrated a runtime error by visiting Genre.aspx and passing in an invalid ID
value through the querystring, like Genre.aspx?ID=foo
. Visiting the page with the Global.asax
file in place produces the same user experience as in the preceding tutorial - in the development environment you'll continue to see the Exception Details Yellow Screen of Death, while in the production environment you'll see the custom error page. In addition to this existing behavior, the developer is sent an email.
Figure 2 shows the email received when visiting Genre.aspx?ID=foo
. The email body summarizes the exception information, while the YSOD.htm
attachment displays the content that is shown in the Exception Details YSOD (see Figure 3).
Figure 2: The Developer Is Sent An Email Notification Whenever There's An Unhandled Exception
(Click to view full-size image)
Figure 3: The Email Notification Includes the Exception Details YSOD As An Attachment
(Click to view full-size image)
What About Using the Custom Error Page?
This tutorial showed how to use Global.asax
and the Application_Error
event handler to execute code when an unhandled exception occurs. Specifically, we used this event handler to notify a developer of an error; we could extend it to also log the error details in a database. The presence of the Application_Error
event handler does not affect the end user's experience. They still see the configured error page, be it the Error Details YSOD, the Runtime Error YSOD, or the custom error page.
It's natural to wonder whether the Global.asax
file and Application_Error
event is necessary when using a custom error page. When an error occurs the user is shown the custom error page so why can't we put the code to notify the developer and log the error details into the code-behind class of the custom error page? While you can certainly add code to the custom error page's code-behind class you do not have access to the details of the exception that triggered the Error
event when using the technique we explored in the preceding tutorial. Calling the GetLastError
method from the custom error page returns Nothing
.
The reason for this behavior is because the custom error page is reached via a redirect. When an unhandled exception reaches the ASP.NET runtime the ASP.NET engine raises its Error
event (which executes the Application_Error
event handler) and then redirects the user to the custom error page by issuing a Response.Redirect(customErrorPageUrl)
. The Response.Redirect
method sends a response to the client with an HTTP 302 status code, instructing the browser to request a new URL, namely the custom error page. The browser then automatically requests this new page. You can tell that the custom error page was requested separately from the page where the error originated because the browser's Address bar changes to the custom error page URL (see Figure 4).
Figure 4: When an Error Occurs the Browser Gets Redirected to the Custom Error Page URL
(Click to view full-size image)
The net effect is that the request where the unhandled exception occurred ends when the server responds with the HTTP 302 redirect. The subsequent request to the custom error page is a brand new request; by this point the ASP.NET engine has discarded the error information and, moreover, has no way to associate the unhandled exception in the previous request with the new request for the custom error page. This is why GetLastError
returns null
when called from the custom error page.
However, it is possible to have the custom error page executed during the same request that caused the error. The Server.Transfer(url)
method transfers execution to the specified URL and processes it within the same request. You could move the code in the Application_Error
event handler to the custom error page's code-behind class, replacing it in Global.asax
with the following code:
Sub Application_Error(ByVal sender As Object, ByVal e As EventArgs)
' Get the error details
Dim lastErrorWrapper As HttpException = _
CType(Server.GetLastError(), HttpException)
If lastErrorWrapper.GetHttpCode() = 404 Then
Server.Transfer("~/ErrorPages/404.aspx")
Else
Server.Transfer("~/ErrorPages/Oops.aspx")
End If
End Sub
Now when an unhandled exception occurs the Application_Error
event handler transfers control to the appropriate custom error page based on the HTTP status code. Because control was transferred, the custom error page has access to the unhandled exception information via Server.GetLastError
and can notify a developer of the error and log its details. The Server.Transfer
call stops the ASP.NET engine from redirecting the user to the custom error page. Instead, the custom error page's content is returned as the response to the page that generated the error.
Summary
When an unhandled exception occurs in an ASP.NET web application the ASP.NET runtime raises the Error
event and displays the configured error page. We can notify the developer of the error, log its details, or process it in some other fashion, by creating an event handler for the Error event. There are two ways to create an event handler for HttpApplication
events like Error
: in the Global.asax
file or from an HTTP Module. This tutorial showed how to create an Error
event handler in the Global.asax
file that notifies developers of an error by means of an email message.
Creating an Error
event handler is useful if you need to process unhandled exceptions in some unique or customized manner. However, creating your own Error
event handler to log the exception or to notify a developer is not the most efficient use of your time as there already exist free and easy to use error logging libraries that can be setup in a matter of minutes. The next two tutorials examine two such libraries.
Happy Programming!
Further Reading
For more information on the topics discussed in this tutorial, refer to the following resources:
- ASP.NET HTTP Modules and HTTP Handlers Overview
- Gracefully Responding to Unhandled Exceptions - Processing Unhandled Exceptions
HttpApplication
Class and the ASP.NET Application Object- HTTP Handlers and HTTP Modules in ASP.NET
- Sending Email in ASP.NET
- Understanding the
Global.asax
File - Using HTTP Modules and Handlers to Create Pluggable ASP.NET Components
- Working with the ASP.NET
Global.asax
File - Working with
HttpApplication
Instances