You want to report and log all errors in a common location, regardless of where they arise within the application.
Incorporate the error handling in methods (described in Recipe 7.1), add code to the
Page_Error
event handler to rethrow the page
errors, and add the code to the Application_Error
event handler to perform the logging and redirection.
In the code-behind class for your ASP.NET pages that need to perform error handling, use the .NET language of your choice to:
Create a Page_Error
event handler.
Rethrow the page errors from within the method (this is needed to
avoid all errors being wrapped with an
HttpUnhandledException
exception).
In the code-behind for global.asax
, use the .NET
language of your choice to:
Create an Application_Error
event handler.
Create a detailed message and write it to the event log.
Redirect the user to the error page using
Server.Transfer
.
The code we’ve written to demonstrate this solution
is shown in Example 7-6 through Example 7-9. The Page_Error
code
required in all pages is shown in Example 7-6 (VB)
and Example 7-7 (C#). The
Application_Error
code required in the
global.asax
code-behind is shown in Example 7-8 (VB) and Example 7-9 (C#).
(Because the .aspx
file for this example
contains nothing related to the error handling, it is not included
here.)
The exception model in ASP.NET provides the ability for exceptions to
be handled at any level, from the method level to the application
level. An unhandled exception is sequentially rethrown to each method
in the call stack. If no methods in the call stack handle the
exception, the Page_Error
event is raised. If the
exception is not handled in the Page_Error
event,
the event is rethrown and the Application_Error
event is raised. The rethrowing of exceptions at the application
level allows for processing in a single location for the application.
To process errors at the application level, each page must include
the Page_Error
event handler with a single line of
code to rethrow the last exception that occurred, as follows:
Private Sub Page_Error(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles MyBase.Error 'rethrow the last error that occurredThrow Server.GetLastError( )
End Sub 'Page_Error private void Page_Error(Object sender, System.EventArgs e) { // rethrow the last error that occurredthrow Server.GetLastError( );
} // Page_Error
Why is this step required? We do this to avoid
having
the exception information wrapped with an
HttpUnhandledException
exception. It turns out
that ASP.NET automatically creates a new
HttpUnhandledException
at the page level unless
you simply rethrow the last exception, which from
ASP.NET’s prospective constitutes handling the
exception.
There is a school of thought that says this step
isn’t necessary and that it’s fine
to have ASP.NET wrap your exceptions at will; all you have to do is
just ignore all the “outer”
exceptions and get the first inner exception. We
don’t subscribe to this view, however, because there
are cases, such as page parse errors, that do not get wrapped with
the HttpUnhandledException
. This can make it
difficult to extract the “real”
exception information when there is no guarantee that the
“real” exception information is the
first inner exception in the chain of exceptions.
Visual Studio .NET users can make the insertion of the
Page_Error
code on each page much easier by either
using the built-in macro facilities or adding the code block for the
Page_Error
event to the toolbox. Alternately, you
can create a base page that contains the
Page_Error
method and have all of your pages
inherit from this base page. With this approach you do not have to
deal with implementing Page_Error
in all of your
pages.
The error processing for the application is placed in the
Application_Error
event handler (in the
global.asax
code-behind). Much of this code
follows a fairly standard pattern, which is illustrated in Example 7-8 (VB) and Example 7-9 (C#).
The first step is to get a reference to the last exception that
occurred:
lastException = Server.GetLastError( ) lastException = Server.GetLastError( );
The next step is to create a detailed message to insert into the
event log. This message should contain the message from the most
recent exception and a complete dump of all error information in the
link list of exceptions. The complete dump is obtained by calling the
ToString
method of the last exception, as in the
following example code:
message =lastException.Message
& _ vbCrLf & vbCrLf & _lastException.ToString( )
message =lastException.Message
+ " " +lastException.ToString( );
Next, you can write the message to the event log. As we show in
Example 7-8 and Example 7-9, this is done by creating a new
EventLog
object, setting the
Source
property to a constant containing the name
of the event source to write the information to (the Application
log), and then writing the message to the event log. When writing the
entry to the event log, the event type can be set to
Error
, FailureAudit
,
Information
, SuccessAudit
, and
Warning
, all of which are members of the
EventLogEntryType
enumeration. Here is the code
responsible for writing to the event log in our example:
Log = New EventLog( ) Log.Source = EVENT_LOG_NAME Log.WriteEntry(message, _ EventLogEntryType.Error) log = new EventLog( ); log.Source = EVENT_LOG_NAME; log.WriteEntry(message, EventLogEntryType.Error);
The event log entry created by our example is shown in Example 7-5. The entry shows that a
NullReferenceException
occurred
at
line 64 in the code-behind for the example page. If the exception had
been wrapped by throwing new exceptions at each error-handling point
in the code, they would all be listed here. This is very useful for
troubleshooting runtime errors because the actual source of the error
is shown, along with the complete call path to the point the error
occurred.
Example 7-5. Event log entry for this example
{System.NullReferenceException}
[System.NullReferenceException]: {System.NullReferenceException}
HelpLink: Nothing
InnerException: Nothing
Message: "Object reference not set to an instance of an object."
Source: "VBExamples"
StackTrace: " at ASPNetCookbook.VBExamples.CH07ApplicationLevelErrorHandlingVB.Page_
Error(Object sender, EventArgs e) in D:ASPNetBookProjectsASPNetCookbookSolution
UIProjectsVBExamplesCH07ApplicationLevelErrorHandlingVB.aspx.vb:line 64
at System.Web.UI.TemplateControl.OnError(EventArgs e) 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)" TargetSite: {System.Reflection.RuntimeMethodInfo}
At this point any other notifications, such as sending an email to the system administrator, should be performed. Refer to Recipe 18.7 for information regarding sending emails.
The final step to processing errors at the application level is to clear the error and redirect the user to the page where an error message is displayed:
Server.ClearError( ) Server.Transfer("CH07DisplayErrorVB.aspx" & _ "?PageHeader=Error Occurred" & _ "&Message1=" & lastException.Message & _ "&Message2=" & _ "This error was processed at the application level") Server.ClearError( ); Server.Transfer("CH07DisplayErrorCS.aspx" + "?PageHeader=Error Occurred" + "&Message1=" + lastException.Message + "&Message2=" + "This error was processed at the application level");
If you do not clear the error, ASP.NET will assume the error has not been processed and will handle it for you with its infamous “yellow” screen.
You can use either of two methods to redirect the user to the error
page. The first method is to call
Response.Redirect
, which works by returning
information to the browser instructing the browser to do a redirect
to the page indicated. This results in an additional round trip to
the server. As we show in Example 7-8 (VB) and Example 7-9 (C#), the second method
is
Server.Transfer
, which is the method we favor
because it transfers the request to the indicated page without the
extra browser/server round trip.
By default, Windows 2000 and 2003 Server provides three event
sources: Application, Security, and System. Of the three sources, the
default ASP.NET user (ASPNET) only has permission to write to the
Application log. Attempts to write to the Security or System logs
will result in an exception being thrown in the
Application_Error
event.
You can make the errors for your application easier to find in the
event viewer by creating an event source specific to your
application. Without escalating the privileges for the ASPNET user to
the System level (not a good option), you cannot create a new event
source within your ASP.NET application. Two options are available.
You can use the registry editor and add a new key to the
HKEY_LOCAL_MACHINESystemCurrentControlSetServicesEventLog
key or create a simple console application to do the work for you. We
suggest the console application because it is easy and repeatable.
Create a console application in the usual fashion, add the following
code to it, and then run the application while logged in as a user
with administrative privileges. This will create an event source
specific to your application. The only change that is required to the
code described here is to change the
EVENT_LOG_NAME
constant value to the name of your
new event source.
Const EVENT_LOG_NAME As String = "Your Application" If (Not EventLog.SourceExists(EVENT_LOG_NAME)) Then EventLog.CreateEventSource(EVENT_LOG_NAME, EVENT_LOG_NAME) End If const String EVENT_LOG_NAME = "Your Application"; if (EventLog.SourceExists(EVENT_LOG_NAME) != null) { EventLog.CreateEventSource(EVENT_LOG_NAME, EVENT_LOG_NAME); }
Example 7-6. Page_Error code for handling errors at the application level (.vb)
'************************************************************************* ' ' ROUTINE: Page_Error ' ' DESCRIPTION: This routine handles the error event for the page. It ' is used to trap all errors for the page and rethrow the ' exception. The rethrow is needed to avoid all errors ' being wrapped with an "HttpUnhandledException" exception. '------------------------------------------------------------------------- Private Sub Page_Error(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles MyBase.Error'rethrow the last error that occurred
Throw Server.GetLastError( )
End Sub 'Page_Error
Example 7-7. Page_Error code for handling errors at the application level (.cs)
//************************************************************************ // // ROUTINE: Page_Error // // DESCRIPTION: This routine provides the event handler for the page // error event. It builds a URL with the error information // then sets the ErrorPage property to the URL. //------------------------------------------------------------------------ private void Page_Error(Object sender, System.EventArgs e) {// rethrow the last error that occurred
throw Server.GetLastError( );
} // Page_Error
Example 7-8. Application_Error code for handling errors at the application level (.vb)
'************************************************************************* ' ' ROUTINE: Application_Error ' ' DESCRIPTION: This routine provides the event handler for the ' application error event. It is responsible for ' processing errors at the application level. '------------------------------------------------------------------------- Sub Application_Error(ByVal sender As Object, ByVal e As EventArgs) Const EVENT_LOG_NAME As String = "Application" Dim lastException As Exception Dim Log As EventLog Dim message As String 'get the last error that occurred lastException = Server.GetLastError( ) 'create the error message from the message in the last exception along 'with a complete dump of all of the inner exceptions (all exception 'data in the linked list of exceptions) message = lastException.Message & _ vbCrLf & vbCrLf & _ lastException.ToString( ) 'Insert error information into the event log Log = New EventLog Log.Source = EVENT_LOG_NAME Log.WriteEntry(message, _ EventLogEntryType.Error) 'perform other notifications, etc. here 'clear the error and redirect to the page used to display the 'error information Server.ClearError( ) Server.Transfer("CH07DisplayErrorVB.aspx" & _ "?PageHeader=Error Occurred" & _ "&Message1=" & lastException.Message & _ "&Message2=" & _ "This error was processed at the application level") End Sub 'Application_Error
Example 7-9. Application_Error code for handling errors at the application level (.cs)
//************************************************************************ // // ROUTINE: Application_Error // // DESCRIPTION: This routine provides the event handler for the // application error event. It is responsible for // processing errors at the application level. //------------------------------------------------------------------------ protected void Application_Error(Object sender, EventArgs e) { const String EVENT_LOG_NAME = "Application"; Exception lastException = null; EventLog log = null; String message = null; // get the last error that occurred lastException = Server.GetLastError( ); // create the error message from the message in the last exception along // with a complete dump of all of the inner exceptions (all exception // data in the linked list of exceptions) message = lastException.Message + " " + lastException.ToString( ); // Insert error information into the event log log = new EventLog( ); log.Source = EVENT_LOG_NAME; log.WriteEntry(message, EventLogEntryType.Error); // perform other notifications, etc. here // clear the error and redirect to the page used to display the // error information Server.ClearError( ); Server.Transfer("CH07DisplayErrorCS.aspx" + "?PageHeader=Error Occurred" + "&Message1=" + lastException.Message + "&Message2=" + "This error was processed at the application level"); } // Application_Error