Approaching Error Handling

One way to approach the understanding of a large subject is to categorize it into smaller chunks. There are five main categories of errors:

■ Syntax errors

■ Parameter/input errors

■ External code errors

■ Resource errors

Your goals in trapping these types of errors should be, in order of importance, the following:

1. Keep your application from crashing.

2. Provide as much information as possible about the error.

3. Propagate errors back to a critical decision point.

4. Degrade gracefully.

In an ASP.NET application, it's very difficult to crash the application—the ASP.NET engine won't let that happen—but by default, any error that occurs on a page causes the ASP.NET engine to stop executing the code on that page. To your users, it doesn't matter whether your application crashed. If users can't accomplish their goals, then your program is broken, no matter what the reason. For error-handling purposes, each page of your application is a program unto itself. Therefore, the first goal is to make the page run properly.

If you can't fix an error in code, the second goal is to write an informative error message to the user, who in response may inform you of the error, and also to a log file or the Application event log. The more information you provide about the error, the easier it will be for you to find and fix it. You don't want your page to stop processing—even with good error messages—if you can work around the error, so you'll want to categorize errors based on how they affect your application. For example, input errors should never be fatal; you should trap them all and provide reasonable error messages so the user or programmer providing the information can solve the problem.

The third goal—to propagate errors back to a critical decision point—refers to the process of trapping errors in lower-level routines. When an error occurs in a low-level routine, the error is trapped and passes upward through the call stack until the error reaches a critical decision point. In an ASP.NET script, the critical decision point is usually the code running on that page.

During development, you should focus primarily on the first two goals. In some cases, you won't have enough information to do more than approach the third goal during initial development. Later in the development cycle, after several users have tested your application, you should be able to improve your error handling to solve common problems. Save the fourth goal until last. It does you no good to spend lots of time degrading gracefully if that causes you to miss fatal errors that crash the application.

Error Handling in C#

Errors in the .NET framework and in C# throw exceptions. As you may expect by now, the framework uses classes derived from a base Exception class. The Common Language Runtime (CLR) common errors derive from a SystemException class, while any custom Exception classes you create should derive from ApplicationException. The basic error-trapping and -handling structure in C# is the try-catch-finally block. For example, the Web Form ch9-3.aspx code-behind file contains code that causes a divide by zero exception. Set it as the start page and run the application. You'll see the error message in Figure 9.15.

Figure 9.15: DivideByZero-Exception error message

Figure 9.15: DivideByZero-Exception error message

Note Causing an exception via dividing by zero in C# is more difficult than you might think. For example, writing the code result = 5 / 0 immediately underlines the 5 / 0 portion of the line, with the warning This constant expression produces a value that is not representable in type 'Integer' . Similarly, using the line Response.Write((5 / 0).ToString()) didn't produce the expected error—instead, the word "Infinity" appeared in the browser. In other words, watch out! You may not get the result you expect, even from this simple, if invalid, operation!

try-catch-finally Exception Handling

To trap the exception, you enclose the code in a try-catch-finally block. The finally clause of the block is optional. For example, in its simplest form—try-catch—the block looks like the following:

int result; int divisor = 0; int dividend = 5; try {

result = dividend / divisor; Response.Write(result.ToString());

catch (Exception ex) {

Response.Write(String.Concat(ex.Message + "<br>"));

Note The try-catch block already exists in the ch9-3.aspx.cs file on www.sybex.com but has been commented out. Uncomment the try-catch block to handle the exception.

The code attempts to execute the statements after the try statement. When the exception occurs, execution jumps to the catch statement. The variable ex "catches" the exception, and execution resumes with the Response.Write statement that displays the error message (ex.Message).

For added safety, you can include code that always executes, whether an error occurs or not. For example, suppose you want to display the HTML from a file named ch9-x.aspx in the browser, but the file doesn't exist. Ignore for a moment the fact that you should always test files before opening them, and—solely for example purposes—pretend that you might actually write code this bad:

FileStream fs = null; try {

fs = File.Open(Page.MapPath("ch9-X.aspx"), FileMode.Open); fs.Close();

catch (Exception ex) {

Response.Write(String.Concat(ex.Message + "<br>"));

finally {

The attempt to open the nonexistent file causes an error. The code creates a FileStream object variable. The try block attempts to open the file, and the catch block displays the error, and then the finally block executes. However, if you simply write fs.Close(), you'll cause another error because the fs variable is Null—it never gets set. In other words, there's nothing special about code in finally blocks that prevents errors from occurring inside them. You must be just as careful with code in a finally block as you would be with any other code. Therefore, the finally block checks explicitly for a null reference before attempting to close the FileStream.

The code shown catches any exception, but you can filter exceptions so that you can respond differently to different errors. To do this, you would include multiple catch blocks, each of which would respond to a different exception or set of exceptions. For example:

FileStream fs = null; try {

fs = File.Open(Page.MapPath("ch9-X.aspx"), FileMode.Open); fs.Close();

catch (FileNotFoundException ex) {

Response.Write(String.Concat(ex.Message + "<br>"));

catch (Exception ex) {

Response.Write(String.Concat(ex.Message + "<br>"));

finally {

Note This code also exists in the ch9-3.aspx.cs file on www.sybex.com but is commented out.

Although each exception has its own class, they all inherit from the base Exception class. While you could create a custom Exception class that implemented additional features, it would not be a good idea because an implicit cast from your custom Exception object to a base Exception object (as shown in the two preceding code examples—catch ex) would fail. Table 9.2 shows the properties and methods of the base Exception object; I've omitted properties and methods that the Exception class inherits from Object.

Table 9.2: Properties and Methods of the Base Exception Object

Property/Method

Description

HelpLink property

URL to an associated help topic.

Hresult property

Sets or returns an hresult—use to retrieve the error code returned from a Windows API call.

InnerException property

May contain an exception thrown by code lower on the stack. Exceptions raised from lower-level code often assign the preceding Exception object to the InnerException property, letting you track the progress of errors as they rise through the call stack.

Message property

A string containing a description of the error.

Source property

A string that describes the object or the application that caused the error. You can set this property explicitly, but by default, the Source property contains the name of the assembly where the error occurred.

StackTrace property

You should be ecstatic over this one—I know I am. The StackTrace shows the execution stack at the time the error

occurred. In many languages prior to .NET, you had to code this feature yourself, which most programmers did not do.

TargetSite property

Returns the method (a MethodBase object) that encapsulates the method that threw the exception. This is extremely helpful because it can show you the parameter types and values for the method when the error occurred.

GetBaseException method

Returns the original (root) exception, thrown by the method returned from the Targetsite property.

GetObjectData method

Used to serialize an exception.

ToString method

You should override this method for custom Exception classes. The System Exception classes return a string containing the name of the exception, the error message, the name of an inner exception, and the stack trace—the information that you see by default in the browser when ASP.NET throws an exception.

Throwing Exceptions

You can throw exceptions as well as catch them. For example, suppose you build a Search class for your application. You want to differentiate between invalid search entries and searches that don't result in any hits; therefore, you can create an InvalidSearchEntry exception class and throw that exception when a user enters an invalid string. This particular application throws a custom invalidSearchEntry exception whenever the user enters a blank search term or one that contains any spaces or capital letters. You must inherit your Exception classes from the System.ApplicationException class.

Listing 9.3 shows the code for the custom InvalidSearchEntry exception class. Listing 9.3: The InvalidSearchEntry Custom Exception Class using System;

namespace CSharpASP.ch9 { /// <summary>

/// Summary description for invalidSearchEntryException. /// </summary>

public class invalidSearchEntryException : ApplicationException { public enum invalidSearchEntryEnum : int { invalidSearchTerm=0, ContainsSpace=1, ContainsCaps=2

invalidSearchEntryEnum m invalidSearchEntry; public invalidSearchEntryException(invalidSearchEntryEnum invalidSearchEntry) {

m_invalidSearchEntry = invalidSearchEntry;

public override String Message { get {

switch (m invalidSearchEntry) {

case invalidSearchEntryEnum.invalidSearchTerm : return "The search engine encountered " + "an invalid search term."; case invalidSearchEntryEnum.ContainsCaps : return "Search terms may not contain " + "capital letters."; case invalidSearchEntryEnum.ContainsSpace :

return "Search terms may not contain spaces."; default :

return "Unknown search error.";

public override String ToString() { System.Text.StringBuilder sb = new System.Text.StringBuilder(1000); sb.Append("<span style='color: red; font-size: 14px'>" +

"<b>Invalid Search Term</b></span><br>"); sb.Append(this.GetType().FullName + "<br>"); sb.Append("<b>Error Number</b>: " + this.HResult + "<br>");

sb.Append("Source</b>: " + this.Source +

"<p>&nbsp;</p>"); sb.Append("<b>Stack Trace</b>: " + "<br>" +

this.StackTrace); return (sb.ToString());

Note that the class overrides the Message and ToString methods inherited from Application-Exception, and that it has two constructors—a default constructor that takes no arguments and a second version that takes one of two InvalidSearchEntryEnum enumeration values: either ContainsSpace or ContainsCaps. The passed enumeration value sets an internal flag that influences the Message property value, letting the error message be more explicit.

Run the ch9-4.aspx Web Form and enter some text into the Search In field. Enter a string to search for into the Search For field and then click the Search button. In the Result field the letters "et" in etudes will appear in red. Try the Web Form with both valid search terms (no spaces or capital letters in the Search For field—see Figure 9.16) and with invalid terms (see Figure 9.17).

Figure 9.16: The ch9-4.aspx Web Form after searching with a valid search term

Figure 9.17: The ch9-4.aspx Web Form error dissplay after searching with an invalid search term

Listing 9.4 shows the code that executes when you click the Search button.

Listing 9.4: Clicking the Search Button in the Web Form ch9-4.aspx Throws an Error if the Search Term Contains Spaces or Capital Letters private void btnSearch Click(object sender, System.EventArgs e) { String srchIn, srchFor; int foundIndex; srchIn = txtSearchIn.Text; srchFor = txtSearchFor.Text; try {

if ((srchIn == "") || (srchFor == "")) { throw new InvalidSearchEntryException

(InvalidSearchEntryException.InvalidSearchEntryEnum. InvalidSearchTerm);

throw new InvalidSearchEntryException (InvalidSearchEntryException.InvalidSearchEntryEnum. ContainsSpace);

else if (srchFor.ToLower() != srchFor) { throw new InvalidSearchEntryException (InvalidSearchEntryException.InvalidSearchEntryEnum. ContainsCaps);

foundIndex = srchIn.IndexOf(srchFor); if (foundIndex >= 0) {

lblResult.Text = srchIn.Substring(0, foundIndex) + "<b><font color='red'>" + srchIn.Substring(foundIndex, srchFor.Length) + "</b></font>" + srchIn.Substring(foundIndex + srchFor.Length);

lblResult.Text = "Search term not found.";

catch (InvalidSearchEntryException ex) { Response.Write(ex.Message + "<br>"); Response.Write(ex.ToString());

Response.End();

In the next section, you'll see how to intercept the standard ASP error-handling mechanism so that you can handle errors yourself or customize the error display.

Intercepting Page-Level Errors

The Page class exposes an Error event, which the ASP.NET engine fires whenever an untrapped error occurs. Interestingly, this event does not show up in the event list in C# for the Page object. The event does not fire for trapped errors. To prove that, you'll override the event. Load the ch9-4.aspx.cs file into the editor and enter an event handler declaration for the Error event:

private void Page Error(object sender, System.EventArgs e) { // error-handling code here

Note The Page_Error event code is commented out on www.sybex.com. Uncomment it to test the code in this section.

At this point, it doesn't matter what the Page_Error subroutine does—it's just important to find out when it fires. Write anything you like into the Page_Error subroutine; for example:

private void Page Error(object sender, System.EventArgs e) { Response.Write("An error event occurred."); Response.End();

You have to add a delegate for the new event. Although Visual Studio places automatically generated delegates in the InitializeComponent method, you should not alter that code, because changes to the Web Form may overwrite your modifications. Instead, create the delegate during the Page_Load method.

this.Error += new System.EventHandler(this.Page Error);

Set a breakpoint on the first Response.Write statement in the Page_Error event (you do have to enter at least one executable line to set the breakpoint), and then run the application. Does the event fire? It shouldn't, because you trapped the errors using the try-catch block in the btnSearch click event code. Comment out the catch block (you'll need to add a finally block to get the code to compile) and then compile and run the page again. This time, the breakpoint should fire, and you should see the output in the browser. An alternate version (commented out in the code) uses the Server.Transfer method to transfer execution to a custom error file.

private void Page Error(object sender, System.EventArgs e) { Response.Write("An error event occurred."); Server.Transfer("customErrors.aspx", false); //Response.End();

Note that you need either the Response.End() line or the Server.Transfer line, but not both. When you run this alternate version, IIS transfers program execution to the customErrors.aspx Web Form to write error information to the client.

The customErrors.aspx file uses the Server.GetLastError method to retrieve the error information for display:

private void Page Load(object sender, System.EventArgs e) { Exception ex;

Response.Write("Error Description: " + ex.Message);

Response.Write("No errors.");

Note Comment out or delete the overridden Page_Error handler and the delegate in the Page_Load event before you continue.

If you don't handle an error at the Page level, you can still trap it at Application scope. Intercepting Errors at Application Scope

If an error slips past your Page-level error handling, you can intercept it in the Global.asax.cs file by implementing the Global_Error event handler. To intercept the errors, override the Global_Error event in the Global.asax.cs file just as you did to intercept Page errors in the preceding section. Because the process is identical, I won't repeat it here. The Global_Error event handler is your last opportunity to handle an error before ASP.NET passes it to the default error handler. At this point, you cannot recover from an error, but you can still make the error display a little friendlier—and you can still log errors (see Chapter 10, "File and Event Log Access with ASP.NET," for more information about logging errors and messages).

Was this article helpful?

0 0

Post a comment