Rich Custom Error Handling with ASP.NET

 

By Eli Robillard

January 2004

Applies to:
   Microsoft® ASP.NET

Summary: Adding your own custom error handling to your ASP.NET Web applications can ease debugging and improve customer satisfaction. Eli Robillard shows how you can create an error-handling mechanism that shows a friendly face to customers and still provides the detailed technical information developers will need. (19 printed pages)

Download the source code for this article.

Contents

Introduction
Errors Raise Exceptions
The Exception Class
Try...Catch...Finally
Page_Error
global.asax: Application_Error
Rich Custom Error Pages
Implementation

Introduction

The quality of a site should be measured not only by how well it works, but by how gracefully it fails. While developers need detailed error reports while debugging, visitors to the site should be shielded from these. Technical errata only serve to confuse, disappoint, and reveal cracks in the armor.

If an error page is displayed, it should serve both developers and end-users without sacrificing aesthetics. An ideal error page maintains the look and feel of the site, offers the ability to provide detailed errors to internal developers—identified by IP address—and at the same time offers no detail to end users. Instead, it gets them back to what they were seeking—easily and without confusion. The site administrator should be able to review errors encountered either by e-mail or in the server logs, and optionally be able to receive feedback from users who run into trouble. Is this the stuff of dreams? No more.

There are several obstacles standing between the current built-in features of ASP.NET 1.1 and realizing the ideal. For one, the built-in detailed error page is fixed and cannot be customized. Further, a custom error page (as set in web.config) does not have access to the last error thrown, so it is really only useful to make the apology prettier. While the customErrors tag in web.config has a property to provide the custom page to external users and the detailed error page only to the local user (mode=RemoteOnly), how many developers debug at the server console?

All these problems can be solved, but first it helps to understand the basics. Peter Bromberg wrote two resources on exception handling that everyone should read as a prerequisite for this article: Documenting Exceptional Developers and Build a Really Useful ASP.NET Exception Engine. In addition, source code is provided with this article (click the link near the top of this page) for a complete rich custom error system. As with my previous MSDN article, the sample is from my favorite fake Swedish journal of finance: Fjorbes Online. All these online resources will introduce you to the basics. Now let's see how to put it all together to bring error handling from the dark ages to the space age.

Errors Raise Exceptions

When errors happen, an exception is raised or thrown. There are three layers at which you may trap and deal with an exception: in a try...catch...finally block, at the Page level, or at the Application level. The first two happen right inside a page's code, and code for application events is kept inside global.asax.

The Exception object contains information about the error, and as the event bubbles up through the layers, it is wrapped in further detail. In rough terms, the Application_Error exception contains the Page_Error exception, which expands on the base Exception, which triggered the bubbling in the first place.

The Exception Class

Not surprisingly, the call to get the last error is Server.GetLastError() and it returns an object of type Exception:

   Dim err As Exception = Server.GetLastError()

You will find an Exception class reference here. Some of the properties and methods of the Exception class are more useful than others. (See Table 1.)

Table 1. Exception class properties and methods rated for usefulness

Property / Method() Return Type Description
Message String The error message. Useful. Available with debugging on or off.
Source String The application or object that caused the error. Not of much use when an .aspx file fails, as .NET generates a random name when it compiles an .aspx into IL (for example, "MyPage.aspx" can become "ecpgatxa"). Reasonably useful for debugging classes and controls.
StackTrace String When execution ends, the stack is unwound. This means that every call from the original page request, down to the line that triggered the error, is popped off the execution stack and noted. Happily, even line numbers are recorded in the base Exception when debugging is turned on. Only the method name is reported when debugging is off. This is highly useful.
TargetSite MethodBase The method that threw the Exception. This also shows up in the StackTrace. It is not as useful on its own.
HelpLink String Can hold a URL that might help the user, but usually doesn't. Consider implementing it when throwing custom exceptions.
InnerException Exception The next error in the StackTrace. You can use InnerException to drill down through the complete list of exceptions to the original Exception.
GetBaseException() Exception The Exception describing the original error. Since the original error might be wrapped up in the depths of InnerException, this method is essential to cut to the chase.
ToString() String Concatenates the above properties into a single string.

To see how the Exception looks as it bubbles up through each layer, consider the following sample Page_Load (with debugging turned on):

Sample Microsoft® Visual Basic® code

Sub Page_Load(ByVal src As Object, ByVal e As EventArgs)
   Throw New ApplicationException("This is an unhandled exception.")
End Sub

Try...Catch...Fail (Exception.Source=" yk1wsaam")

Message: "This is an unhandled exception."

Stack Trace

at ASP.Default_aspx.Page_Load(Object src, EventArgs e) in C:\dev\CustomErrors\Default.aspx:line 5

Page_Error (Exception.Source=" yk1wsaam")

Message: "This is an unhandled exception."

Stack Trace

at ASP.Default2_aspx.Page_Load(Object src, EventArgs e) in C:\dev\CustomErrors\Default.aspx:line 5 
at System.Web.UI.Control.OnLoad(EventArgs e) 
at System.Web.UI.Control.LoadRecursive() 
at System.Web.UI.Page.ProcessRequestMain()

Application_Error (Exception.Source="System.Web")

Message: "Exception of type System.Web.HttpUnhandledException was thrown."

Stack Trace

at System.Web.UI.Page.HandleError(Exception e) 
at System.Web.UI.Page.ProcessRequestMain() 
at System.Web.UI.Page.ProcessRequest() 
at System.Web.UI.Page.ProcessRequest(HttpContext context) 
at System.Web.CallHandlerExecutionStep.System.Web.HttpApplication+IExecutionStep.Execute() 
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

At the Try...Catch...Fail layer, only the immediate Exception exists and Exception.Source is a random string of characters that ASP.NET uses to identify the IL version of Default.aspx.

At the Page level, the stack has unwound to show that the error happened during Page.ProcessRequestMain.

At the Application layer, something interesting happens. The Source changes to the slightly meaningful, "System.Web." Everything above Page.ProcessRequestMain from the Page level exception has been rolled into the Page.HandleError(exception e) line. At the Application layer, that detail is still available by either using InnerException or getting straight to the original exception with GetBaseException().

Try...Catch...Finally

The sooner you catch and resolve an error, the better. There is a reason they are called exceptions and not rules. In most cases, you can stop problems before they become exceptions with simple validation. FileIO.File.Exists and String.Empty are your friends. When writing a new class, always provide a way to validate data. For example, this article's sample code includes an IP address class (IPAddress.vb). While methods IsInRange, ToLong, and ToIPAddress can throw exceptions, the Validate and IsEmptyOrZero functions do not. No one should rely on Try...Catch...Fail for validation.

When writing a line or block of code that could fail based on an uncontrollable condition, such as an unavailable object (like a missing database connection or Web service), it should be wrapped with Try...Catch...Finally.

Inside the Catch block, you have the option of throwing the exception higher to a Page and perhaps an Application error handler. While you can still recover gracefully (that is, without an apology) inside a Page_Error handler, you are not likely to write one for every page. No, the main reason to throw the Exception higher than the Catch block is to perform notification or logging in an Application_Error handler.

When throwing an Exception higher, don't:

Throw New ApplicationException(e)

Do:

Throw

The former is the syntax for a brand new custom Exception. In a Catch you already have one in front of you. The latter is all you need to send the current Exception merrily up the chain of command for further handling. If you must add your own two cents, you can always:

   Catch e As System.Exception
      Throw New System.ApplicationException("My Two Cents", e)

This creates a new Exception that wraps up the original error as its InnerException. Now for a proper demonstration of Try...Catch...Finally.

Visual Basic:

Dim Conn as New _
SqlConnection("Server=sql.mySite.com;uid=myUser;password=myPass")
Dim Cmd As New _
SqlCommand("SELECT Title, URL FROM Article ORDER BY Title", Conn)
Dim IsDbAvailable as Boolean = True
try
   ' The database may not be available
Conn.Open
ArticleGrid.DataSource = Cmd.ExecuteReader
ArticleGrid.DataBind
catch e as Exception
   ' Executed if an error occurs
   IsDbAvailable = False
   Trace.Write ("Database unavailable with Message: ", e.Message)
Trace.Write ("Stack Trace: ", e.StackTrace)

   ' Throw the exception higher for logging and notification
   Throw
finally
   ' If any clean-up is required for either case (unmanaged objects 
   ' left open, etc.), do it here.
Conn.Close()
end try   

C#:

New SqlConnection("Server=sql.mySite.com;uid=myUser;password=myPass") Conn;
New SqlCommand("SELECT Title, URL FROM Article ORDER BY Title", Conn) Cmd;

Boolean IsDbAvailable = True;
try {
   // The database may not be available
Conn.Open();
ArticleGrid.DataSource = Cmd.ExecuteReader();
ArticleGrid.DataBind();
}
catch (e as Exception) {
   // Executed if an error occurs
   IsDbAvailable = False;
   Trace.Write ("Database unavailable with Message: ", e.Message);
   Trace.Write ("Stack Trace: ", e.StackTrace);

   // Throw the exception higher for logging and notification
   throw;
}
finally {
   /* If any clean-up is required for either case (unmanaged objects 
      left open, etc.), do it here. */
   Conn.Close ()
}

You can have multiple Catch blocks, each overloaded to catch a different Exception type (Exception, IndexOutOfRangeException, NullReferenceException, and so on). Chris Sully wrote an article, Error Handling in ASP.NET..., which both provides a reference table of exception types and makes terrific further reading.

Before going on, note that there is a school of thought that says you should always throw an exception higher, that "swallowing" the exception is a bad idea. The reasoning is that exceptions should be visible and logged so that they can be guarded against in the future (with improved infrastructure, scalability, and so on), and that any instances that might be swallowed could be avoided with better programming. This happens to be true. Swallowing an exception is fine while debugging, but production code should always throw exceptions higher. Then, whenever preventable exceptions show up in the logs, you can apply some validation to ensure they won't happen again.

Page_Error

Page_Error and Application_Error are similar. They take the same arguments, and they can even contain the same code. In fact a Page_Error section on a test page is a great way to debug code intended for Application_Error.

Their differences are few. One goes inside the .aspx file (or its code-behind), and the other inside global.asax (or an IHttpHandler class assembly). Differences between their stack traces were noted earlier, but if you always use GetBaseException(), then you will always have the original error, making this difference irrelevant. Another point to note is that in Page_Error you can prevent an exception from bubbling up further on to Application_Error by invoking Context.ClearError(). While useful for debugging (to avoid clogging the error log), this is not good practice in production code.

The following sample code adds the error to the trace information. The page will not finish loading once the exception is raised, so a brief apology is presented. Note that this simply shows the mechanics of Page_Error; actually helping the user out is an exercise left to the reader.

Visual Basic:

Sub Page_Load(ByVal src As Object, ByVal args As EventArgs)
   Throw New ApplicationException("This is an unhandled exception.")
End Sub
   
Sub Page_Error(ByVal src As Object, ByVal args As EventArgs) Handles MyBase.Error
   Dim e As System.Exception = Server.GetLastError()
   Trace.Write("Message", e.Message)
   Trace.Write("Source", e.Source)
   Trace.Write("Stack Trace", e.StackTrace)
   Response.Write("Sorry, an error was encountered.")
Context.ClearError()
End Sub

C#:

void Page_Load(Object src, EventArgs args) {
   // raise an intentional exception to get the ball rolling
   throw new ApplicationException("This is an unhandled exception.");
}

void Page_Error(Object sender, EventArgs args) {
   Response.Write("Error:\n");
   Exception e = Server.GetLastError();
   Trace.Write("Message",e.Message);
   Trace.Write("Source",e.Source);
   Trace.Write("Stack Trace",e.StackTrace);
Response.Write("Sorry, an error was encountered.");
   Context.ClearError();
}

Some people believe that putting code into an override of the Page's OnError event is equivalent to putting it in the Page_Error event. It isn't. OnError hands control to a private HandleError method. This is where ASP.NET checks whether customErrors is turned on and redirects when an exception is raised. It is where ASP.NET checks whether tracing is turned on and adds its own bit about the exception just raised. The main reason to override OnError is to replace this behavior with your own (described later). There is no good reason to put other code there. If someone does decide to turn on the built-in customErrors, code sitting in OnError will get in the way. If someone decides to add code to Page_Error it will not fire, as OnError fires first. To do error-handling at the Page level, use Page_Error.

global.asax: Application_Error

Generating e-mail notification, logging errors to the Event Log, and the structure of global.asax are covered wonderfully in many books and articles. Here are two good references:

The source code provided with the article implements these in global.asax and uses these web.config declarations:

<appSettings>
  <add key="customErrorAutomaticLogging" value = "On/Off" />
  <add key="customErrorAutomaticEmail" value="On/Off"/>
  <add key="customErrorEmailAddress" value="errors@mySite.com" />
</appSettings>

The interesting part is the construction of rich error messages.

Rich Custom Error Pages

There are four pieces to the puzzle:

  1. Configuring behavior in web.config. The choices are either to use the built-in customErrors tag with its mode and defaultRedirect properties, or to build custom settings in the appSettings section.
  2. Capturing, logging, and storing the Exception (in global.asax). The Exception needs to be stored inside an object that will persist until the custom error page. Possibilities are: Application, Context, Cookies, and QueryString. Other notification (e-mail, pager, and so on) can occur here too.
  3. Passing control from global.asax to the custom error page. Methods include using the built-in customErrors method, Server.Transfer(), or Response.Redirect().
  4. Retrieving and displaying the custom error message. Logic to display detailed information only to certain IP addresses may be included here.

Aa479319.customerrors_01(en-us,MSDN.10).gif

Figure 1. Flow of custom error handler

Unfortunately, you cannot pick and choose among all these options; some can only be implemented certain ways. For example, you cannot store the Exception in Context.Items and retrieve it after a Response.Redirect() since the Redirect() creates a brand new Context. The Exception would disappear. Here is a list of which storage baskets work with which control-passing methods:

Storage Basket Control-passing methods that work
Application Response.Redirect(), Server.Transfer(), or customErrors:defaultRedirect
Cookies Response.Redirect(), Server.Transfer(), or customErrors:defaultRedirect
Context, Session Server.Transfer()
QueryString Response.Redirect() or Server.Transfer()

While Application works with all three, it does not scale without a way to identify which session triggered the error. If two users trigger nearly simultaneous errors, you do not want one to overwrite the other. An advantage of Application is that it can store the complete Exception object.

Cookie storage also works with all three, and the client that triggered the error is guaranteed to see the proper error message. The caveats are that cookies must be enabled on the client, and they place an extra burden on bandwidth, which raises a scalability issue. Since you can only store strings and not complete objects to a cookie, you need to decide which strings to pass. XML serialization of an Exception is prevented by internal security issues, but binary or custom serialization is an option.

Context and Session, while limited to Server.Transfer(), both provide the advantage that the entire Exception object may be stored, without the client identification steps required to store the object to Application.

QueryString is an interesting option first described by Donny Mack in ASP.NET: Tips, Tutorials and Code (Mitchell, Mack, Walther, et al., SAMS Publishing, 2001). While you can't stuff a complete Exception into QueryString and must choose which strings to pass, it does avoid local storage overhead completely and would seem to scale the best.

The sample code provided with this article implements all of these methods except Session, which would not be an improvement on Context.

Implementation

Now let's look in more detail at the four steps mentioned above to construct rich custom error pages.

Configuring Behavior

The customErrors setting has three options for the mode property: On, Off, and RemoteOnly. The defaultRedirect property sets the custom error page. You can use customErrors in combination with your own appSettings, which you might do to control events in global.asax (like event logging). ASP.NET transfers control from global.asax to the custom page by generating a default OnError method in the Page's event cycle. You can replace the generated method with your own. The code goes something like this (if "myErrorPage.aspx" is the defaultRedirect value):

Visual Basic:

Protected Overrides Sub OnError(ByVal args As EventArgs)
    Response.Redirect("myErrorPage.aspx?aspxerrorpath=" & _
        Request.Path, True)
End Sub

C#:

protected override void OnError(EventArgs args) {
    Response.Redirect("myErrorPage.aspx?aspxerrorpath=" + 
        Request.Path, true);
}

The True tells ASP.NET to end the current request and proceed. Omitting it would imply the default false, and require an extra line to explicitly call Response.End.

Unfortunately, there is no easy way to override the Page OnError event for all pages on a site (the ideal way to replace customErrors), short of subclassing Page, which would require a new reference at the top of each page. The source code provided uses a different method, relying instead on custom appSettings inside web.config:

   <appSettings>
      <add key="customErrorAutomaticLogging" value="On/Off" />
      <add key="customErrorAutomaticEmail" value="On/Off" />

      <add key="customErrorMethod" 
value="Application/Context/Cookie/QueryString/Off" />
      <add key="customErrorPage" value="myErrorPage.aspx" />
<add key="customErrorBranchMethod" value="Redirect/Transfer" />
      <add key="customErrorAllowReport" value="On/Off" />
      <add key="customErrorEmailAddress" value="errors@mySite.com" />
</appSettings> 

Using appSettings in code is as simple as this:

if (System.Configuration.ConfigurationSettings.AppSettings _
("customErrorAutomaticLogging").ToLower) = "on"

Settings can be added for other functions. For example, you might need to define a connection string for a central error log on a particular server.

Capturing, Logging, and Storing

The top of global.asax imports three namespaces: System.IO, System.Diagnostics, and Msdn.ErrorIO. The Msdn.ErrorIO class is provided in the download as ErrorIO.vb.

This line of Application_Error captures the Exception and converts it to a string for logging or notification:

   Dim objError As Exception = Server.GetLastError.GetBaseException

In C# this is written over two lines:

   Exception objError = Server.GetLastError();
objError = objError.GetBaseException();

It was shown that the Page and Application layers stuff the original Exception in their own Exception wrappers as the call stack unwinds. While Server.GetLastError returns this whole wrapped package, GetBaseException pulls out the original Exception that interrupted execution.

The error is next logged and e-mailed to an administrator depending on web.config settings (see the section, global.asax: Application_Error). Functions WriteErrorToLog() and EmailError() are included in global.asax. Note that their catch blocks are empty, effectively swallowing any problems with writing to the event log or sending e-mail. This is done to avoid throwing a new Exception inside the handler and either creating an endless loop or having ASP.NET default to its own handler. However, it is never preferred to swallow errors. One solution would be to store a message (for example, HandlerMessage) along with the Exception to describe any problems with the exception handler itself. Such a message could appear with the original error on the custom error page. The WriteErrorToLog and EmailError functions allow for this by returning a true or false based on their success, though this feature is not implemented in the source provided.

Four storage models are implemented in the source code. You will find each represented by a class in ErrorIO.vb: ErrorApplication, ErrorCookie, ErrorContext, and ErrorQueryString. Since all classes use the same methods (Store, Retrieve, and Clear), a single interface (IErrorIOHandler) is provided for all three, plus an ErrorIOFactory class with a Create(model) method that allows the storage model to be selected at runtime. The advantage of using the Factory Pattern here is that you can declare the model to use in web.config and switch between them freely. Switching to a new model does not require changing any source code, only the web.config declaration. Of course you can still explicitly choose a model by creating an instance of, say, ErrorContext and using its Store, Retrieve, and Clear methods directly.

In the source, the storage basket is created like this:

   Dim objErrorIOFactory As New Msdn.RichErrors.ErrorIOFactory
   Dim objErrorBasket As MSDN.RichErrors.IErrorIOHandler
   objErrorBasket = objErrorIOFactory.Create(strErrorMethod)

Yes, it takes three lines to create an ErrorIOFactory, create a storage basket, and to connect the object created by the Factory to the basket. It's the price of flexibility. As an alternative, you can hardcode the type of basket used, like so:

   Dim objErrorBasket as ErrorContext = New ErrorContext()

Instead of ErrorContext, you could similarly create an instance of ErrorApplication, ErrorCookie, or ErrorQueryString. Whichever you choose, this is how the exception is stored:

   Dim strRedirect, strQueryString, strFilePath As String 
   strRedirect = AppSettings("customErrorPage")
   strQueryString = objErrorBasket.Store(objError)
   strFilePath = strRedirect & strQueryString

Hmmm, that probably wasn't what you expected. You may be asking, "What's this about building strFilePath? The result of objErrorBasket.Store() is a QueryString?!" Well, back in the section on configuring behavior, you saw the default ASP.NET way of implementing customErrors, and it went like this:

Response.Redirect("myErrorPage.aspx?aspxerrorpath=" & Request.Path, True)

To recreate this behavior, each Store() method in the ErrorIO classes generates the aspxerrorpath parameter. The ErrorQueryString class goes a step further and stores parts of the error (Message, Source, and StackTrace) plus a DateTime stamp in additional parameters. Combining the customErrorPage defined in web.config with the query string generated during the Store() results in a string that can be used with either Response.Redirect(strFilePath) or Server.Transfer(strFilePath).

Refer to the source to see exactly how each of the four storage baskets work; each has unique traits. When naming Application identifiers, ErrorApplication.Store() appends the client IP address so each client is sure to receive his own message. ErrorCookie.Store() uses one multi-part cookie rather than four separate cookies to get the job done. ErrorContext.Store() is the simplest the bunch; nothing tricky about it. QueryString.Store() uses a fast StringBuilder to generate the query string, which it initializes as 512 chars (the default is 16) to cut down on internal resizing steps.

Since some of the storage baskets provided store strings and not objects, the decision was made to store: Exception.Message, Exception.Source, Exception.StackTrace, the Date.Now marking the Exception, and the Request.Filepath (the Web page requested). The source can be modified to record other properties of Exception, the server name, client IP, or whatever meets your debugging requirements.

The web.config <appSettings> tag to turn rich custom errors on and set the storage basket is:

<add key="customErrorMethod" 
value="Application/Cookie/Context/QueryString/Off" />

Setting this value to Off prevents the remaining steps from occurring, but will not get in the way of any automatic logging or notification already done.

Passing Control

The final task of Application_Error is to execute the Redirect() or Transfer(). From the discussion of configuring behavior above, you already know that which of these you choose is tied to how the Exception is stored, and that some combinations work while others don't. The features of the storage methods usually drive the decision.

But occasionally, features of Redirect() and Transfer() drive the decision. Redirect creates a new Context, Transfer does not. Redirect requires a round-trip to the browser, Transfer does not. As a result of this round-trip, Redirect rewrites the URL to reflect the location of the error page, Transfer does not.

If this seems to be an argument in favor of Transfer, it isn't. The built-in customErrors feature uses Redirect and not Transfer for a reason. The rationale of the ASP.NET development team is that Redirect accurately displays the URL of the custom error page, while Server.Transfer is intended for "switchboard"-style pages (as on content management sites) where the true URL is preferably hidden.

Therefore, while Context is one of the more convenient ways to move the Exception from Application_Error to the rich error page, its dependence on Server.Transfer() makes it less than perfect. While you may not be concerned that this approach breaks a tenet of the design philosophy of ASP.NET, be aware that it does.

The web.config <appSettings> tags to set the control-passing method and identify the custom error pages are:

<add key="customErrorBranchMethod" value="Redirect/Transfer" />
<add key="customErrorPage" value ="myErrorPage.aspx" />

Retrieving and Displaying

Finally, the Exception must be retrieved from its storage basket and displayed in the rich error page. In the sample, this page is called myErrorPage.aspx.

In the sample code, the Exception is retrieved in myErrorPage.aspx, inside the DisplayDetailedError() function.

   Dim objErrorIOFactory As New Msdn.ErrorIO.ErrorIOFactory
   Dim objErrorBasket As Msdn.ErrorIO.IErrorIOHandler
objErrorBasket = objErrorIOFactory.Create( _ 
System.Configuration.ConfigurationSettings.AppSettings("customErrorMethod") )
   objErrorBasket.Retrieve(strMessage, strSource, 
     strStackTrace, strDate, strQueryString)
   objErrorBasket.Clear()

The first three lines are familiar; a similar series was used to create the basket to Store() the Exception.

Retrieve() passes five parameters by reference. These will come back holding the exception data. The actual Retrieve methods (in ErrorIO.vb) are similar to their Store() counterparts. The big difference is the use of Try...Catch...Fail to swallow errors that might occur while retrieving data from the baskets. Again, it is not desirable for an exception handler to throw exceptions of its own. If the Retrieve() fails, the error data returned instead describes the type of retrieve attempted (Application, Cookie, Context, or QueryString). Try...Catch...Fail is not used in QueryString.Retrieve(), it being the only technique that cannot generate exceptions of its own.

Finally objErrorBasket.Clear is called. Two of the storage baskets can or should be destroyed explicitly. Since Application is unique for each client (it uses the IP Address to name each identifier), it must be destroyed once used, and even then there is a chance this technique will leak memory.

So too should cookies be destroyed. Cookies are set to expire in thirty minutes, but a user can trigger any number of errors in this span. Thirty seconds would make more sense, but the expiration is based on the server's clock, not the client's. Have you seen how far off some system clocks are? Thirty minutes is a realistic term, most clocks should be within that span of each other. But this is the problem with short-term cookies. Explicit destruction is the best answer.

ErrorContext.Clear() and ErrorQueryString.Clear() have nothing to do, as these baskets ceases to exist once the rich error page is emitted to the client.

The sample rich error page has three display features:

  • Standard Error Message. The standard message, intended for public consumption, explains that an error occurred while loading the requested page and provides a link to try loading the page again. Links are also provided to get the user back to familiar ground—in this case back to the home page. If your site has standard navigation or menu bars, be sure to include them in the error page as well.

    The last thing you want is for someone to hit a brick wall of no return. Get the user back on track. Some sites parse the page request to seed a search of either the current site or the whole Web with a message like, "We couldn't service your request but maybe this will help." Helpful is good.

  • Error Report Panel. A form asking the user what was expected when the error happened is a good way to trace issues. This sample panel says, "If you describe what you were trying to do, perhaps it can be fixed." Reports are e-mailed to the administrator along with the Exception.ToString data. The code in SendClick(), which sends the actual message, is similar to the EmailError() function in global.asax. This feature is configured with the web.config <appSettings> keys:

    <add key="customErrorAllowReport" value="On"/>
    <add key="customErrorEmailAddress" value="errors@mySite.com"/>
    

  • Detailed Error Panel. This panel (populated in the DisplayDetailedError() function) reports the exception raised by the error, as retrieved from the storage basket. This information is useful to those debugging the site, but not the general public. While you could add authentication to distinguish the two, not all sites need authentication. The built-in customErrors feature has a switch (mode="remoteOnly") to only display detailed information to the client at 10.0.0.1, but developers rarely have access to the server console (Why are so many errors only reproducible on the live server?). The way to overcome this limitation is to make a custom IP address class with range checking. Table 2 shows the methods for this class.

    Table 2. IPAddress Class (namespace: Msdn.IPUtils, source: IPAddress.vb)

    Method Description
    Validate() Test for a valid IP address with a regular expression.
    IsInRange() Check an IP address against a specified range.
    ToString() Return a value of type String.
    ToLong() Returns the IP address as a 12-digit Long (for example, "10.0.0.255" becomes 010000000255). Helpful for a variety of comparisons.
    ToIPAddress() Returns a value of type System.Network.IPAddress.

    This panel is displayed if the client's IP address is within the range declared by web.config <appSettings>:

    <add key="customErrorIpRangeMin" value="10.0.0.1"/>

    <add key="customErrorIpRangeMax" value="10.0.0.255"/>

The Return of Page_Error

Debugging custom error pages is a pain. In addition to a local Page_Error handler, it helps to turn on tracing and debugging so you can capture and display problems without throwing the Exception all the way up to Application_Error. Once your custom error page works, turn these off again. While you can just forget about Page_Error, you will be glad it's there during routine maintenance, or whenever you add new errors.

About the Author

Eli Robillard is a .NET guru based in Toronto, Ontario. He is a recognized leader in the developer community, a member of the Microsoft MVP program, and a founding board member of the ASPInsiders. The ASPInsiders provide feedback to the ASP.NET development team on present and future versions of the ASP.NET platform. Eli and his wife Marcie (a.k.a. Datagrid Girl) are both top .NET consultants providing consulting services, training, and team mentoring to organizations working with .NET-based technologies. Eli's Web site provides more information.

Acknowledgements

Thanks to Rob Howard (Microsoft) and Mitch Denny (Monash.NET) for their invaluable thoughts and suggestions.

Resources

© Microsoft Corporation. All rights reserved.