www.gibmonks.com

Main Page

  Previous Section Next Section

10.1 Error Handling

The goal of error handling (also known as exception handling) is quite simple: to prevent exceptions or errors thrown during the execution of an application request from reaching users. Ideally, users should not know that an exception occurred, or they should at least be provided with an informative message that tells them what they can do to resolve the problem. ASP.NET provides three techniques for achieving this goal:

Custom error pages

Allow you to assign one or more error pages to be displayed when an exception occurs.

Page_Error and Application_Error events

Writing event handlers for either or both of these events allows you to catch and handle exceptions at the page or application level.

Structured exception handling

New to Visual Basic .NET, and also available in C#, this type of exception handling allows exceptions to be caught and handled in particular blocks of code.

These three techniques provide broadest (custom error pages, which can handle exceptions from any page in the application) to narrowest (structured exception handling, which handles exceptions for a specific block of code) coverage for handling application exceptions. Figure 10-1 illustrates the relationship of these exception handling techniques to both the exception (shown at the center) and the user, who you're trying to prevent from encountering the exception.

Figure 10-1. Exception handling techniques
figs/anet2_1001.gif

The following sections describe these techniques and explain how they fit into an ASP.NET application. Note that you can use all three techniques together, individually, or in whatever combination you like. Using all three techniques in combination would provide broad coverage for most exceptions and more robust specific exceptions handling, but at the cost of maintaining your exception-handling logic in more places.

10.1.1 Custom Error Pages

The most general, but arguably the simplest, technique for handling exceptions in ASP.NET applications is to implement one or more custom error pages. You can do this by creating a web page to display an error message to the user. Then you specify that page as the default error page (or to handle a specific class of error) in web.config, using the <customError> configuration element. Example 10-1 shows a web.config file that defines a default custom error page called Error.aspx. Example 10-2 shows the custom error page itself, which simply displays the path of the page on which the error occurred. Example 10-3 shows the code for a page that will generate a NullReferenceException (which has an HTTP status code of 500).

Example 10-1. Enabling custom errors in web.config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
   <system.web>
      <customErrors defaultRedirect="Error.aspx" mode="On" />
   </system.web>
</configuration>
Example 10-2. Error.aspx
<%@ Page Language="VB" %>
<html>
<head>
   <title>Error page</title>
</head>
<body>
<h1>Error page</h1>
Error originated on: <%=Request.QueryString("aspxerrorpath") %>
</body>
</html>
Example 10-3. Throw500.aspx
<%@ Page Language="VB" Debug="True" %>
<html>
<head>
   <title>Throw an Error</title>
   <script runat="server">
      Sub Page_Load( )
         Dim NullText As String = Nothing
         Message.Text = NullText.ToString( )
      End Sub
   </script>
</head>
<body>
   <asp:label id="Message" runat="server"/>
</body>
</html>

Instead of the On mode setting for the <customErrors> element, you can set the mode to RemoteOnly or Off (note that these values are case-sensitive). Off will cause detailed error messages containing information about an unhandled exception to be returned to the client, regardless of whether the request is local or remote. Since you don't want users to see error messages if you can avoid it, it's best not to use this value in a production application. RemoteOnly (the default) displays detailed error messages for requests originating from the local host, but displays custom errors (based on the <customErrors> section) to remote clients. On displays the custom error page(s) you specify to any client, regardless of whether the request is local or remote. Using RemoteOnly is a good practice for production applications, since it prevents potentially sensitive information or source code from being displayed to clients, while allowing administrators or developers to view the page locally to read this information.

In addition to providing a default error page, the <customErrors> element also supports the use of child <error> elements to specify custom error pages for specific classes of errors, such as authentication (HTTP 403) or Not Found (HTTP 404) errors, as shown in the following code snippet:

<customErrors defaultRedirect="Error.aspx" mode="On">
   <error statusCode="403" redirect="ErrorAccessDenied.aspx"/>
   <error statusCode="404" redirect="ErrorNotFound.aspx"/>
</customErrors>

Any errors for which there is not a specific <error> element defined are handled by the page specified by the defaultRedirect attribute. Having different error pages for specific errors allows you to provide more informative messages to users, and perhaps offer some instructions on how the errors can be remedied, while still providing a generic handler for errors outside the scope of the specified handlers.

Another important thing you can do in a custom error page, whether specific or generic, is provide logging or notification of the error so that the site developer or administrator knows that there is a problem and can take action to fix it. In ASP.NET, this process is fairly simple and can be accomplished through the use of the MailMessage and SmtpMail classes, which reside in the System.Web.Mail namespace. Example 10-4 shows a custom error page that uses these classes to notify a site administrator of the error and the page on which it occurred.

Example 10-4. Error_SendMail.aspx
<%@ Page Language="VB" %>
<%@ Import Namespace="System.Web.Mail" %>
<html>
<head>
   <title>Error page</title>
   <script runat="server">
      Sub Page_Load( )
         Dim Mail as New MailMessage( )
         'Change the values below to valid email addresses
         Mail.To = "<valid email address>"
         Mail.From = "<valid email address>"
         Mail.Subject = "aspnetian.com error"
         Mail.Body = "An Exception occurred in page " & _
            Request.QueryString("aspxerrorpath")
         'If your SMTP server is not local, change the property below
         '   to a valid server or domain name for the SMTP server
         SmtpMail.SmtpServer = "localhost"
         SmtpMail.Send(Mail)
      End Sub
   </script>
</head>
<body>
<h1>Error page</h1>
Error originated on: <%=Request.QueryString("aspxerrorpath") %>
<br/>
An email has been sent to the administrator of this site notifying them of the error.
</body>
</html>

For the code in Example 10-4 to work, you need to provide valid email addresses for the To and From properties of the MailMessage object instance.

If an unhandled exception occurs in a custom error page, no further redirect will occur, so the user will see a blank page. This situation makes it extremely important for you to ensure that no unhandled exceptions occur in custom error pages. For example, it might be a good idea to wrap the call to SmtpMail.Send in Example 10-4 in a Try...Catch block) to handle potential problems with connecting to the specified SMTP server. For more information about using the Try...Catch block, see Section 10.1.3, later in this chapter.

The advantage of using custom error pages is that it allows you to handle a lot of errors from a single location (or a small number of locations). The disadvantage is that there's not much you can do to handle the error, other than display a helpful message and notify someone that an error occurred. The reason for this is that you don't have access to the actual exception object in a custom error page, which means you can neither display information about the specific exception nor take steps to handle it.

10.1.2 Page_Error and Application_Error

Another technique for error handling that provides the ability to handle a broad range of application errors is the use of the Error event defined by the Page and HttpApplication classes. Unless AutoEventWireup has been set to False (the default in Web Forms pages created with Visual Studio .NET), ASP.NET automatically calls page or application-level handlers with the name Page_Error or Application_Error if an unhandled exception occurs at the page or application level, respectively. The handler for Page_Error should be defined at the page level, as shown in Example 10-5, while the handler for Application_Error should be defined in the application's global.asax file, as shown in Example 10-6.

Example 10-5. Throw500_Page_Error.aspx
<%@ Page Language="VB" %>
<%@ Import Namespace="System.Web.Mail" %>
<html>
<head>
   <title>Throw an Error</title>
   <script runat="server">
      Sub Page_Load( )
         Dim NullText As String = Nothing
         Message.Text = NullText.ToString( )
      End Sub
      Sub Page_Error(Source As Object, E As EventArgs)
         Dim ex As Exception = Server.GetLastError( )
         If Not ex Is Nothing Then
            Dim Mail as New MailMessage( )
            'Change the values below to valid email addresses
            Mail.To = "<valid email address>"
            Mail.From = "<valid email address>"
            Mail.Subject = "aspnetian.com error"
            Mail.Body = "An Exception occurred in page " & _
               Request.RawUrl & ":" & vbCrLf
            Mail.Body &= ex.ToString( ) & vbCrlf & vbCrlf
            Mail.Body &= "was handled from Page_Error."
            'If your SMTP server is not local, change the property below
            '   to a valid server or domain name for the SMTP server
            SmtpMail.SmtpServer = "localhost"
            SmtpMail.Send(Mail)
            Server.ClearError( )
         End If
         Response.Write("An error has occurred. " & _
            "The site administrator has been notified.<br/>" & _
            "Please try your request again later.")
      End Sub
   </script>
</head>
<body>
   <asp:label id="Message" runat="server"/>
</body>
</html>

Example 10-5 deliberately causes a NullReferenceException exception by calling ToString on an object that is set to Nothing. In Page_Error, we retrieve this exception by calling Server.GetLastError. The example then creates and sends an email that includes the exception details (calling ToString on an exception object returns the error message and the call stack as a string).

Finally, the code clears the exception by calling Server.ClearError. This last step is important because neither the Page_Error nor the Application_Error handler clears the exception by default. If you don't call ClearError, the exception will bubble up to the next level of handling. For example, if you define both a Page_Error handler at the page level and an Application_Error handler in global.asax, and you do not call ClearError in Page_Error, the Application_Error handler is invoked in addition to Page_ Error. This can be a useful behavior if expected—for example, if you wish to use Page_Error to generate useful messages, while using Application_ Error to log all errors or send notifications. If you're not expecting it, though, this behavior can be confusing, to say the least.

Example 10-6 does essentially the same thing as Example 10-5, but handles errors at the application level, rather than at the page level. You can still access the Server object to get the exception that was thrown. Since Application_Error may handle exceptions for web services as well as for Web Forms pages, Example 10-6 does not attempt to use Response.Write to send a message to the user.

Example 10-6. global.asax
<%@ Import Namespace="System.Web.Mail" %>
<script language="VB" runat="server">
   Sub Application_Error(sender As Object, e As EventArgs)
      Dim ex As Exception = Server.GetLastError( )
      If Not ex Is Nothing Then
         Dim Mail as New MailMessage( )
         'Change the values below to valid email addresses
         Mail.To = <valid email address>
         Mail.From = <valid email address>
         Mail.Subject = "aspnetian.com error"
         Mail.Body = "An Exception occurred in page " & _
            Request.RawUrl & ":" & vbCrLf
         Mail.Body &= ex.ToString( ) & vbCrlf & vbCrlf
         Mail.Body &= "was handled from Application_Error."
         'If your SMTP server is not local, change the property below
         '   to a valid server or domain name for the SMTP server
         SmtpMail.SmtpServer = "localhost"
         SmtpMail.Send(Mail)
         Server.ClearError( )
      End If
   End Sub
</script>

10.1.3 Structured Exception Handling

The most specific technique for exception handling, and the most useful in terms of gracefully recovering from the exception, is structured exception handling. Structured exception handling should be familiar to developers of Java and C++, for which it is standard practice, but it is new to the Visual Basic .NET language. Microsoft's new language, C#, also provides built-in support for structured exception handling.

In structured exception handling, you wrap code that may throw an exception in a Try...Catch block, as shown in the following code snippet:

'VB.NET
Try
   ' Code that may cause an exception
Catch ex As Exception
   ' Exception handling code
Finally
   ' Code executes whether or not an exception occurs
End Try
  
//C#
try
{
   // Code that may cause an exception
}
catch (Exception ex)
{
   // Exception handling code
}
finally
{
   // Code executes whether or not an exception occurs

The Try statement (lowercase try in C#) warns the runtime that the code contained within the Try block may cause an exception; the Catch statement (catch in C#) provides code to handle the exception. You can provide more than one Catch statement, with each handling a specific exception, as shown in the following code snippet. Note that each exception to be handled must be of a type derived from the base Exception class:

'VB.NET
Try
   ' Code that may cause an exception
Catch nullRefEx As NullReferenceException
   ' Code to handle null reference exception
Catch ex As Exception
   ' Generic exception handling code
End Try
  
//C#
try
{
   // Code that may cause an exception
}
catch (NullReferenceException nullRefEx)
{
   // Code to handle null reference exception
}
catch (Exception ex)
{
   // Generic exception handling code
}

When using multiple Catch blocks, the blocks for specific exceptions should always appear before any Catch block for generic exceptions, or the specific exceptions will be caught by the generic exception handler.

The Finally statement (finally in C#) is also useful in structured exception handling. When used in conjunction with a Try...Catch block, the Finally statement allows you to specify code that will always be run regardless of whether an exception is thrown. This can be especially useful if you need to run clean-up code that might not otherwise run if an exception occurred, such as code that closes a database connection and/or rolls back a database transaction to avoid leaving data in an inconsistent state. Example 10-7 shows a page that attempts to connect to the Pubs SQL Server database and execute a command that returns a SqlDataReader. If either the connection attempt or the command results in an exception, the code in the Catch block will be executed. The code in the Finally block tests to see if the data reader and/or connection are open. If they are, it closes them.

Example 10-7. ReadTitles.aspx
<%@ Page Language="VB" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<html>
   <title>Try-Catch-Finally Example</title>
   <head>
      <script runat="server">
         Sub Page_Load( )
            Dim ConnStr As String = "Data Source=(local)\NetSDK;" & _
               "Initial Catalog=Pubs;Trusted_Connection=True;"
            Dim SQL As String = "SELECT title, price FROM title " & _ 
               "WHERE PRICE IS NOT NULL"
            Dim PubsConn As New SqlConnection(ConnStr)
            Dim TitlesCmd As New SqlCommand(SQL, PubsConn)
            Dim Titles As SqlDataReader
            Try
               PubsConn.Open( )
               Titles = TitlesCmd.ExecuteReader( )
               Output.Text = "<table>"
               While Titles.Read( )
                  Output.Text &= "<tr>"
                  Output.Text &= "<td>" & Titles.GetString(0) & "</td>"
                  Output.Text &= "<td>$" & _
                     Format(Titles.GetDecimal(1), "##0.00") & "</td>"
                  Output.Text &= "</tr>"
               End While
               Output.Text &= "</table>"
            Catch sqlEx As SqlException
               Response.Write("A SqlException has occurred.")
            Catch ex As Exception
               Response.Write("An Exception has occurred.")
            Finally
               If Not Titles Is Nothing Then
                  If Not Titles.IsClosed Then
                     Titles.Close( )
                  End If
               End If
               If PubsConn.State = ConnectionState.Open Then
                  PubsConn.Close( )
               End If
            End Try
            Response.Write("<br/>The current connection state is: " & _
               PubsConn.State.ToString( ) & ".")
         End Sub
      </script>
   </head>
<body>
   <h1>SqlDataReader Example</h1>
   <asp:label id="Output" runat="server"/>
</body>
</html>

As you can see from the examples in this section, of the available exception-handling techniques, structured exception handling is likely to require the most code to implement. However, it also provides you with the ability to handle the exception transparently to the user in cases when it is possible to recover from the exception. For example, you could modify the code in Example 10-6 to test whether the exception was related to the attempt to open the connection and, if so, retry the connection a predefined number of times. This way, if the exception is the result of a temporary network problem, the exception handling code can potentially handle this problem without the user ever being aware that a problem exists (apart from the slight delay in connecting).

      Previous Section Next Section