asp:feature
LANGUAGES: VB .NET | C#
TECHNOLOGIES: Error Handling | CLR | Exceptions
Build Generic Error Handlers
Learn how to create centralized error handling in your applications to foster a "gentler" user experience.
By Don Kiely
As a longtime Visual Basic user, I've always been a bit appalled that the unstructured On Error handler was my only way to write robust, bulletproof code. VBScript took error handling to new lows, limiting my ASP pages only to inline error handling. As a long reformed C++ programmer, I knew there were better ways.
With .NET and its Common Language Runtime (CLR), all the .NET languages sport structured error handling. Microsoft also endowed ASP.NET with some great features for recovering gracefully when things in your Web application go awry. Combining these ASP.NET features with those in the .NET Framework can yield amazingly robust applications.
Here's the best part: You finally have all the tools you need to create centralized error handling in Web applications. Even better, you can do it without writing and rewriting a ton of scripting code like you had to back in the old days of ASP. In this article, I'll explain how I built just such a generic error handler for unhandled errors that, despite your best efforts, slip through the cracks in an application. My solution isn't the prettiest - no one ever accused me of being a graphics designer - but I hope the code and ASP.NET techniques it employs provide a foundation that works for the kind of apps you write. Hopefully you'll hatch ideas of your own about other ways to make your app's user experience more pleasant - even when things go wrong. Along the way, I'll touch on and use some advanced techniques from throughout the .NET Framework. I'll talk about the ways that I put a technology to use, although most of them deserve an article of their own if not an entire book.
ASP.NET and the CLR: Working Together
In my article Kill Bugs Dead!, I covered the basic error-handling techniques you should use throughout a Web application. These build on the principle that you generally should handle most exceptions in software as close to the source of the problem as possible. So, in a Web page, you can handle exceptions at the procedure level, with the .NET Try-Catch-Finally blocks; at the Page level, with the Error event or a Page directive; at the Application level, with the Error event in global.asax; or you simply can punt and let ASP.NET fall back on its default exception handling. (You can customize default handling via settings in web.config, including redirecting users to a Web page of your own design if an unhandled exception slips through.)
That article covered the basics, so now I'm going to put them to use. I had several design requirements for the centralized error handler, including the information available to the user and to the developer. For the user:
- A user-friendly explanation in English of what the problem is, whether it is solvable, and what can be done
- Date and time of the exception
- Ways the user can get immediate help if needed
- A field where the user can enter pertinent information about what they were doing when the problem arose
- A field where the user can enter contact information in case the developer needs to reach them
Although that kind of information is useful, it doesn't give the developer much to go on to solve the problem. But giving the user more can be a serious breech of security. Displaying stack traces and other information can impress a user with a developer's incredible intelligence, though it virtually guarantees someone will hack the application. So the developer needs a lot more information to solve the problem without exposing it to the user:
- Stack trace
- Information about the environment and application state
- Whether it is a database exception
- Connection and command information
- Custom information specific to an application and the current procedure
- Everything else that will help the developer solve the problem hours after the user has logged off the system
- Options configurable through web.config
- The ability to intrude on the ASP.NET application with as light a touch as possible
Figure 1 shows the solution I devised. When an unhandled exception rears its ugly head, the application presents the user with a warm and fuzzy Web page explaining that something went horribly wrong, using English or the local vernacular of choice and minimal technobabble.
Figure 1. This is the Web page the user gets when the application
experiences an unhandled exception. Notice that, in the interest of security
and protection from the bad guys, it doesn't display much technical
information.
The user can opt to hit the browser's back button to try the operation again - optimist! - or send an error report to the development team. In these days of heightened awareness of security and privacy issues, I didn't want simply to include a Submit button that sent, from the user's perspective, all kinds of personal information. So the user can click on the View Report Before Submitting button to view the information at the bottom of the page (see Figure 2).
Figure 2. Few users will understand the gobbledygook the application
will send to the development team, but at least they can review it and make
sure it isn't transmitting their Charles Schwab online account password.
The exception page intrudes on the ASP.NET application with as light a touch as possible. I'm sure there are probably a few better ways of doing things, but this version of the page minimizes customizations in the application. It does so by including customization settings in the AppSettings section of web.config, one line of code added to global.asax, and one module added to the project (well, really two with the ASPX page's code-behind file). If you were really determined to minimize the touch, you probably could work this down a bit.
Create Custom Exceptions
The System.Exception class is the base class for all exceptions built into the .NET Framework. Whenever something goes wrong in a .NET application, the CLR raises the most appropriate exception for the problem. All these exceptions derive from either System.SystemException or System.ApplicationException, which are, in turn, derived from System.Exception. Figure 3 displays a small portion of the dozens of exceptions defined in the .NET Framework.
Figure 3. System.Exception is the mother of all .NET exceptions. Most of
the built-in exceptions derive from System.SystemException, but you should
derive your own custom exceptions from System.ApplicationException. There's no
functional difference between the two; it's simply a way to keep exceptions
organized.
Normally, you should do your best to handle exceptions raised in and by your code. But there are three exceptions that cause special concerns: StackOverflowException, OutOfMemoryException, and ExecutionEngineException. As their names suggest, these are catastrophic exceptions from which you can't recover. You can catch them in a Try-Catch-Finally block, but you always should re-throw them so ASP.NET terminates the application. Even if you catch these exceptions and do something you think will help, your application - and perhaps ASP.NET itself - will be in an unstable state. Don't tempt fate on these.
Even if you are throwing your own error in response to a problem at run time, try to use one of the built-in .NET exceptions if you can. Microsoft has done all the work for you, and you can add your own customized error message to the exception. For example, say you have a procedure with a division calculation. If the divisor is zero and you don't catch the exception, the CLR raises DivideByZeroException, which derives from ArithmeticException. You can wrap a custom exception around the .NET exception and customize it to your heart's content. An exception's InnerException property still gives you access to the next exception up in a hierarchy of nested exceptions, and GetBaseException accesses the raw exception that caused the problem in the first place.
Deriving a new exception is what I did in the generic error handler. Start by creating a new class in your ASP.NET project (all the code in this article is written in VB .NET, but you can download a C# version as well):
Public Class aspnetPROException
Inherits ApplicationException
All .NET exceptions are serializable, so you can remote
them across process boundaries, and the
Inheriting from ApplicationException, and thereby from System.Exception, gets you many nice features for your custom exception. In particular, you inherit the InnerException property for nesting exceptions; the Message property that provides a description of the exception; and support for serialization.
I wanted to carry along several other pieces of information along with the custom exception, so I defined private class variables for things such as the application name, date and time of the occurrence, and contact information for the responsible developer so the user could reach someone for more information. I also added the appropriate Property procedures to expose these pieces of information to the rest of the application.
When building custom exceptions, you should include at least three constructors that mirror the underlying Exception objects, as well as a fourth if you need to support serialization. First is the default constructor; nothing terribly interesting here:
Public Sub New()
MyBase.New()
End Sub
Second, it is common to instantiate an exception and pass in a string for the value of the Message property. This is one way to use built-in exceptions while customizing the description the user sees:
Public Sub New(ByVal sMessage As String)
MyBase.New(sMessage)
End Sub
The third constructor lets you pass in a custom message as well as a reference to another exception. This is useful when the application has raised an exception, you've caught it, and you want to raise a custom exception of your own. You can grant the code that handles the error farther up the call stack access to the original error by providing a value for the eInner parameter:
Public Sub New(ByVal sMessage As String, _
ByVal eInner As Exception)
MyBase.New(sMessage, eInner)
End Sub
The fourth constructor is optional, but it is required if you plan to support serialization, which is useful for passing objects within and between applications. The underlying exception objects are serializable already, but you must provide support for any custom properties you implement as part of the custom exception. I won't cover this any further here, but the serialization constructor is included with this article's code download.
The rest of the code in the aspnetPROException class exposes the Message and StackTrace properties of the base class, as well as Property procedures to expose the custom properties.
Many of the original values used to populate the custom properties of the class come from web.config in the AppSettings section. (This application would be a good use of custom application sections, but I'll save that topic for another article.) Here are the initial settings:
value="asp.netPRO
Exception Page" /> value="[email protected]" /> You'll need to customize these for use with your
environment and application. I'll mention each of these settings as we come to
them in the code. At this point, you have a custom exception called
aspnetPROException. The namespace for this project is aspnetPROExceptions, so
the full object reference is aspnetPROExceptions.aspnetPROException. You easily
can encapsulate the custom exception in its own namespace, then use a standard
name in your code when referencing the exception. So now you're writing the application and come to a
situation where you need a Try-Catch-Finally block and a custom exception. The
sample project does this in the Click event procedure of a button on a Web
form. It's highly contrived, but it gets the idea across. In this case, the
variable y in the Try block is zero, which raises a DivideByZeroException and a
jump to the appropriate Catch block: Try z = x / y zi = x \ y Catch exc As DivideByZeroException Dim aspExc As New _ aspnetPROException(
_ "y is zero
in the Big Calculation", exc) aspExc.PlainDesc =
"The value you entered ..." Throw aspExc End Try The Catch block that catches the DivideByZeroException
exception first instantiates a new instance of the aspnetPROException. Notice
that this uses the third form of the constructor, passing in a custom message
and the original exception the code raised. Then the code assigns a
technobabble-free description of the problem to one of the custom properties,
PlainDesc. Finally, the code throws the custom exception up to whatever code
will handle it. But there is no custom code to handle it because this
exception is thrown from an event procedure. Events are not called by other
code, strictly speaking, so ASP.NET must handle the exception as best it can. Deal With Unhandled ASP.NET
Exceptions So far, you've created a custom class that inherits from
ApplicationException in the .NET Framework. You can use it anywhere you need
it, either in response to an exception built into the Framework (as the
previous code demonstrates) or anytime a procedure needs to raise a custom
exception. All this applies to any kind of .NET application, not only ASP.NET. In an ASP.NET application, when an unhandled exception
occurs you generally want to display some kind of page that indicates a problem
as gently and helpfully as possible. In the previous click event procedure
code, had I simply displayed a message on the current page about something the
user could fix rather than raising the aspExc exception, it would have been a
handled exception. You must be very clear on the distinction. At this point, you can use any of ASP.NET's many
mechanisms to respond to the unhandled exception, most of which I discussed in
my previous article. For this generic error-handling page, I saved the
exception to a global variable, gEXC, in the Application_Error event procedure
in global.asax. I also set the default redirect setting in web.config to
ErrorPage.aspx, which is the generic error page. I just as easily could have
done a Response.Redirect in the Application_Error procedure or used another
technique for ensuring the exception is available in the ErrorPage module. I declared gEXC as type Exception so the page could be
used with any kind of unhandled exception. The first thing ErrorPage.aspx does
when it loads is assign gEXC to a module variable, casting it to the
aspnetPROException type, so the code is able to access the custom properties of
the class: Dim mEXC As aspnetPROException = _ CType(gEXC,
aspnetPROException) The Page_Load event procedure in ErrorPage.aspx basically
does user-interface stuff, presenting the information shown in Figure 1. It
reads the custom properties of the mEXC object variable and puts them on the
page. It also checks to see whether the AllowUserPreview setting allows users
to preview the error information that will be sent if they submit it. If the
setting is "Yes," the code makes the View Report Before Submitting button
visible. The rest of the code in ErrorPage.aspx reads information
from web.config so it knows where to send an e-mail, whether it should record
something in the event log, and so forth. It also saves the exception
information to a .dat file in a Reports subdirectory of the application. This code uses a BuildErrorReport procedure to build a
text version of the exception with all the technobabble a developer needs to
diagnose and solve the problem. The code in that procedure reads properties of
the mEXC object and uses a StringBuilder object to concatenate it all together
into a single string. It adds the description, name, phone number, and e-mail
address the user entered on the form (if any). It also digs into the exception,
recording the message, stack trace, and other information provided by the base
Exception object - not stuff you want the user to see, unless they really want
to! Most importantly, BuildErrorReport is a recursive
procedure. Any exception can have an InnerException property, and the
aspnetPROException contains the original exception raised when the code
attempted to divide by zero. So, if InnerException is not Null (C#) or Nothing
(VB .NET), the procedure calls itself, passing in the exception reference by
the InnerException property along with an incremented iLevel value to keep
track of how deeply the call is nested: If Not exc.InnerException Is Nothing
Then sb.Append(BuildErrorReport(exc.InnerException, _ iLevel + 1)) End If The ViewReport procedure also calls a BuildReportStream
procedure, which serializes mEXC to both a memory stream and a file stream. The
memory stream is required for attaching the exception to an EventLog entry, and
the file stream is needed to attach it to an e-mail message. The latter file is
named using the current SessionID and a .dat extension. This is all pretty
simple with .NET's Stream objects. Once the report streams are available, ViewReport either
sends them on their way if the user clicked on the Submit button, or it simply
blasts them to a previously invisible textbox for the user to review (but not
change). If the user chooses to submit the report, the code either writes an
Event Log entry or sends an e-mail - or both, depending on the settings in
web.config. That code is pretty standard, so I won't cover it here. One end result is the event log entry shown in Figure 4.
Notice that because I attached the memory stream, the event log entry contains
the byte data you can review. The e-mail sent contains the error report as the
text of the e-mail message and the full serialized exception object as a .dat
file attachment. With either technique, you easily could write an application
to re-create the exception object and examine it more closely for clues about
what went wrong. The reports are sent when the user clicks on the Submit
button, then the ViewReport procedure redirects to an ErrorResult.aspx page.
This is a static page that thanks the user, presents a warm and fuzzy image to
contrast with exploding volcanoes, and tells them how to return to the
application. And life is sweet once again - except now you must solve the
problem! The techniques I've presented in this article are only the
beginning, providing a foundation for creating bulletproof applications. Or, at
least, when they do get shot they can rebound gracefully and still present
users with Internet nirvana: a pleasing Web experience. The code referenced in this
article is available for download. Don Kiely is senior technology consultant for
Information Insights, a business and technology consultancy in Fairbanks,
Alaska. E-mail him at mailto:[email protected]. Tell us what you think! Please send any comments about
this article to [email protected].
Please include the article title and author.
Figure 4. Using the Windows event log is a useful technique for
gathering exception information from an ASP.NET application. You can view the
results in the Computer Management applet, or you can access them
programmatically and rehydrate the exception.