It is common sense to take a method and try it. If it fails, admit it frankly and try another. But above all, try something. | ||
--Franklin Delano Roosevelt |
O! throw away the worser part of it, And live the purer with the other half. | ||
--William Shakespeare |
If they’re running and they don’t look where they’re going I have to come out from somewhere and catch them. | ||
--J. D. Salinger |
In this chapter you’ll learn:
<objective>What exceptions are and how they’re handled.
</objective> <objective>When to use exception handling.
</objective> <objective>To use try
blocks to delimit code in which exceptions might occur.
To throw
exceptions to indicate a problem.
To use catch
blocks to specify exception handlers.
To use the finally
block to release resources.
The .NET exception class hierarchy.
</objective> <objective>Exception
properties.
To create user-defined exceptions.
</objective> </feature><feature> <supertitle>Outline</supertitle> </feature>In this chapter, we take a deeper look at exception handling. As you know from Section 8.4, an exception indicates that a problem occurred during a program’s execution. The name “exception” comes from the fact that, although the problem can occur, it occurs infrequently. If the “rule” is that a statement normally executes correctly, then the occurrence of a problem represents the “exception to the rule.” As we showed in Section 8.4 and in Chapter 10, exception handling enables you to create applications that can handle exceptions—in many cases allowing a program to continue executing as if no problems were encountered. More severe problems may prevent a program from continuing normal execution, instead requiring the program to notify the user of the problem, then terminate in a controlled manner. The features presented in this chapter enable you to write clear, robust and more fault-tolerant programs (i.e., programs that are able to deal with problems that may arise and continue executing). The style and details of C# exception handling are based in part on the work of Andrew Koenig and Bjarne Stroustrup. “Best practices” for exception handling in Visual C# are specified in the Visual Studio documentation.[1].
After reviewing exception-handling concepts and basic exception-handling techniques, we overview .NET’s exception-handling class hierarchy. Programs typically request and release resources (such as files on disk) during program execution. Often, the supply of these resources is limited, or the resources can be used by only one program at a time. We demonstrate a part of the exception-handling mechanism that enables a program to use a resource, then guarantee that it will be released for use by other programs, even if an exception occurs. We show several properties of class System.Exception
(the base class of all exception classes) and discuss how you can create and use your own exception classes.
Let’s revisit what happens when errors arise in a console application that does not use exception handling. Figure 13.1 inputs two integers from the user, then divides the first integer by the second using integer division to obtain an int
result. In this example, an exception is thrown (i.e., an exception occurs) when a method detects a problem and is unable to handle it.
Example 13.1. Integer division without exception handling.
1 // Fig. 13.1: DivideByZeroNoExceptionHandling.cs 2 // Integer division without exception handling. 3 using System; 4 5 class DivideByZeroNoExceptionHandling 6 { 7 static void Main() 8 { 9 // get numerator and denominator 10 Console.Write( "Please enter an integer numerator: " ); 11 int numerator = Convert.ToInt32( Console.ReadLine() ); 12 Console.Write( "Please enter an integer denominator: " ); 13 int denominator = Convert.ToInt32( Console.ReadLine() ); 14 15 // divide the two integers, then display the result 16 int result = numerator / denominator; 17 Console.WriteLine( " Result: {0:D} / {1:D} = {2:D}", 18 numerator, denominator, result ); 19 } // end Main 20 } // end class DivideByZeroNoExceptionHandling
Please enter an integer numerator: 100 Please enter an integer denominator: 7 Result: 100 / 7 = 14 |
Please enter an integer numerator: 100 Please enter an integer denominator: 0 Unhandled Exception: System.DivideByZeroException: Attempted to divide by zero. at DivideByZeroNoExceptionHandling.Main() in C:examplesch13Fig13_01DivideByZeroNoExceptionHandling DivideByZeroNoExceptionHandling DivideByZeroNoExceptionHandling.cs: line 16 Please enter an integer numerator: 100 Please enter an integer denominator: hello Unhandled Exception: System.FormatException: Input string was not in a correct format. at System.Number.StringToNumber(String str, NumberStyles options, NumberBuffer& number, NumberFormatInfo info, Boolean parseDecimal) at System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo info) at DivideByZeroNoExceptionHandling.Main() in C:examplesch13Fig13_01DivideByZeroNoExceptionHandling DivideByZeroNoExceptionHandling DivideByZeroNoExceptionHandling.cs: line 13 |
In most of our examples, the application appears to run the same with or without debugging. As we discuss shortly, the example in Fig. 13.1 might cause errors, depending on the user’s input. If you run this application using the Debug > Start Debugging menu option, the program pauses at the line where an exception occurs, displays the Exception Assistant and allows you to analyze the current state of the program and debug it. We discuss the Exception Assistant in Section 13.3.3. We discuss debugging in detail in Appendix G.
In this example, we do not wish to debug the application; we simply want to see what happens when errors arise. For this reason, we execute this application from a Command Prompt window. Select Start > All Programs > Accessories > Command Prompt to open a Command Prompt window, then use the cd
command to change to the application’s Debug
directory. For example, if this application resides in the directory C:examplesch13Fig13_01DivideByZeroNoExceptionHandling
on your system, you will type
cd /d C:examplesch13Fig13_01DivideByZeroNoExceptionHandling DivideByZeroNoExceptionHandlinginDebug |
in the Command Prompt, then press Enter to change to the application’s Debug
directory. To execute the application, type
DivideByZeroNoExceptionHandling.exe |
in the Command Prompt, then press Enter. If an error arises during execution, a dialog is displayed indicating that the application encountered a problem and needs to close. In Windows Vista and Windows 7, the system tries to find a solution to the problem, then asks you to choose between looking online for a solution to the problem and closing the program. [Note: On some systems a Just-In-Time Debugging dialog is displayed instead. If this occurs, simply click the No button to dismiss the dialog.] At this point, an error message describing the problem is displayed in the Command Prompt. We formatted the error messages in Fig. 13.1 for readability. [Note: Selecting Debug > Start Without Debugging (or <Ctrl> F5) to run the application from Visual Studio executes the application’s so-called release version. The error messages produced by this version of the application may differ from those shown in Fig. 13.1, because of optimizations that the compiler performs to create an application’s release version.]
The first sample execution shows a successful division. In the second, the user enters 0
as the denominator. Several lines of information are displayed in response to the invalid input. This information—known as a stack trace—includes the name of the exception class (System.DivideByZeroException
) in a message indicating the problem that occurred and the path of execution that led to the exception, method by method. This information helps you debug a program. The first line of the error message specifies that a DivideByZeroException
occurred. When a program divides an integer by 0, the CLR throws a DivideByZeroException
(namespace System
). The text after the name of the exception, “Attempted to divide by zero
,” indicates why this exception occurred. Division by zero is not allowed in integer arithmetic. [Note: Division by zero with floating-point values is allowed and results in the value infinity—represented by either constant Double.PositiveInfinity
or constant Double.NegativeInfinity
, depending on whether the numerator is positive or negative. These values are displayed as Infinity
or -Infinity
. If both the numerator and denominator are zero, the result of the calculation is the constant Double.NaN
(“not a number”), which is returned when a calculation’s result is undefined.]
Each “at
” line in a stack trace indicates a line of code in the particular method that was executing when the exception occurred. The “at
” line contains the namespace, class and method in which the exception occurred (DivideByZeroNoExceptionHandling.Main
), the location and name of the file containing the code (C:examplesch13Fig13_01DivideByZeroNoExceptionHandlingDivideByZeroNoExceptionHandlingDivideByZeroNoExceptionHandling.cs
) and the line number (:line 16
) where the exception occurred. In this case, the stack trace indicates that the DivideByZeroException
occurred when the program was executing line 16 of method Main
. The first “at
” line in the stack trace indicates the exception’s throw point—the initial point at which the exception occurred (i.e., line 16 in Main
). This information makes it easy for you to see where the exception originated, and what method calls were made to get to that point in the program.
In the third sample execution, the user enters the string “hello
” as the denominator. This causes a FormatException
, and another stack trace is displayed. Our earlier examples that read numeric values from the user assumed that the user would input an integer value, but a noninteger value could be entered. A FormatException
(namespace System
) occurs, for example, when Convert
method ToInt32
receives a string that does not represent a valid integer. Starting from the last “at
” line in the stack trace, we see that the exception was detected in line 13 of method Main
. The stack trace also shows the other methods that led to the exception being thrown. To perform its task, Convert.ToInt32
calls method Number.ParseInt32
, which in turn calls Number.StringToNumber
. The throw point occurs in Number.StringToNumber
, as indicated by the first “at
” line in the stack trace. Method Convert.ToInt32
is not in the stack trace because the compiler optimized this call out of the code—all it does forward its arguments to Number.ParseInt32
.
In the sample executions in Fig. 13.1, the program terminates when exceptions occur and stack traces are displayed. This does not always happen—sometimes a program may continue executing even though an exception has occurred and a stack trace has been printed. In such cases, the application may produce incorrect results. The next section demonstrates how to handle exceptions to enable the program to run to normal completion.
Now, let’s consider a simple example of exception handling. The application in Fig. 13.2 uses exception handling to process any DivideByZeroException
s and FormatException
s that might arise. The application reads two integers from the user (lines 18–21). Assuming that the user provides integers as input and does not specify 0
as the denominator for the division, line 25 performs the division and lines 28–29 display the result. However, if the user inputs a noninteger value or supplies 0
as the denominator, an exception occurs. This program demonstrates how to catch and handle such exceptions—in this case, displaying an error message and allowing the user to enter another set of values.
Example 13.2. FormatException
and DivideByZeroException
handlers.
1 // Fig. 13.2: DivideByZeroExceptionHandling.cs 2 // FormatException and DivideByZeroException handlers. 3 using System; 4 5 class DivideByZeroExceptionHandling 6 { 7 static void Main( string[] args ) 8 { 9 bool continueLoop = true; // determines whether to keep looping 10 11 do 12 { 13 // retrieve user input and calculate quotient 14 try 15 { 16 // Convert.ToInt32 generates FormatException 17 // if argument cannot be converted to an integer 18 Console.Write( "Enter an integer numerator: " ); 19 int numerator = Convert.ToInt32( Console.ReadLine() ); 20 Console.Write( "Enter an integer denominator: " ); 21 int denominator = Convert.ToInt32( Console.ReadLine() ); 22 23 // division generates DivideByZeroException 24 // if denominator is 0 25 int result = numerator / denominator; 26 27 // display result 28 Console.WriteLine( " Result: {0} / {1} = {2}", 29 numerator, denominator, result ); 30 continueLoop = false; 31 } // end try 32 catch ( FormatException formatException ) 33 { 34 Console.WriteLine( " " + formatException.Message ); 35 Console.WriteLine( 36 "You must enter two integers. Please try again. " ); 37 } // end catch 38 catch ( DivideByZeroException divideByZeroException ) 39 { 40 Console.WriteLine( " " + divideByZeroException.Message ); 41 Console.WriteLine( 42 "Zero is an invalid denominator. Please try again. " ); 43 } // end catch 44 } while ( continueLoop ); // end do...while 45 } // end Main 46 } // end class DivideByZeroExceptionHandling
Please enter an integer numerator: 100 Please enter an integer denominator: 7 Result: 100 / 7 = 14 |
Enter an integer numerator: 100 Enter an integer denominator: 0 Attempted to divide by zero. Zero is an invalid denominator. Please try again. Enter an integer numerator: 100 Enter an integer denominator: 7 Result: 100 / 7 = 14 |
Enter an integer numerator: 100 Enter an integer denominator: hello Input string was not in a correct format. You must enter two integers. Please try again. Enter an integer numerator: 100 Enter an integer denominator: 7 Result: 100 / 7 = 14 |
Before we discuss the details of the program, let’s consider the sample outputs in Fig. 13.2. The first sample output shows a successful calculation in which the user enters the numerator 100
and the denominator 7
. The result (14
) is an int
, because integer division always yields an int
result. The second sample output demonstrates the result of an attempt to divide by zero. In integer arithmetic, the CLR tests for division by zero and generates a DivideByZeroException
if the denominator is zero. The program detects the exception and displays an error message indicating the attempt to divide by zero. The last sample output depicts the result of inputting a non-int
value—in this case, the user enters “hello
” as the denominator. The program attempts to convert the input string
s to int
s using method Convert.ToInt32
(lines 19 and 21). If an argument cannot be converted to an int
, the method throws a FormatException
. The program catches the exception and displays an error message indicating that the user must enter two ints
.
Another way to validate the input is to use the Int32.TryParse
method, which converts a string
to an int
value if possible. All of the numeric types have TryParse
methods. The method requires two arguments—one is the string
to parse and the other is the variable in which the converted value is to be stored. The method returns a bool
value that’s true
only if the string
was parsed successfully. If the string
could not be converted, the value 0
is assigned to the second argument, which is passed by reference so its value can be modified in the calling method. Method TryParse
can be used to validate input in code rather than allowing the code to throw an exception.
Now we consider the user interactions and flow of control that yield the results shown in the sample output windows. Lines 14–31 define a try
block enclosing the code that might throw exceptions, as well as the code that’s skipped when an exception occurs. For example, the program should not display a new result (lines 28–29) unless the calculation in line 25 completes successfully.
The user inputs values that represent the numerator and denominator. The two statements that read the int
s (lines 19 and 21) call method Convert.ToInt32
to convert string
s to int
values. This method throws a FormatException
if it cannot convert its string
argument to an int
. If lines 19 and 21 convert the values properly (i.e., no exceptions occur), then line 25 divides the numerator
by the denominator
and assigns the result to variable result
. If denominator
is 0
, line 25 causes the CLR to throw a DivideByZeroException
. If line 25 does not cause an exception to be thrown, then lines 28–29 display the result of the division.
Exception-handling code appears in a catch
block. In general, when an exception occurs in a try
block, a corresponding catch
block catches the exception and handles it. The try
block in this example is followed by two catch
blocks—one that handles a FormatException
(lines 32–37) and one that handles a DivideByZeroException
(lines 38–43). A catch
block specifies an exception parameter representing the exception that the catch
block can handle. The catch
block can use the parameter’s identifier (which you choose) to interact with a caught exception object. If there’s no need to use the exception object in the catch
block, the exception parameter’s identifier can be omitted. The type of the catch
’s parameter is the type of the exception that the catch
block handles. Optionally, you can include a catch
block that does not specify an exception type—such a catch
block (known as a general catch
clause) catches all exception types. At least one catch
block and/or a finally
block (discussed in Section 13.5) must immediately follow a try
block.
In Fig. 13.2, the first catch
block catches FormatException
s (thrown by method Convert.ToInt32
), and the second catch
block catches DivideByZeroException
s (thrown by the CLR). If an exception occurs, the program executes only the first matching catch
block. Both exception handlers in this example display an error-message dialog. After either catch
block terminates, program control continues with the first statement after the last catch
block (the end of the method, in this example). We’ll soon take a deeper look at how this flow of control works in exception handling.
An uncaught exception (or unhandled exception) is an exception for which there’s no matching catch
block. You saw the results of uncaught exceptions in the second and third outputs of Fig. 13.1. Recall that when exceptions occur in that example, the application terminates early (after displaying the exception’s stack trace). The result of an uncaught exception depends on how you execute the program—Fig. 13.1 demonstrated the results of an uncaught exception when an application is executed in a Command Prompt. If you run the application from Visual Studio with debugging, and the runtime environment detects an uncaught exception, the application pauses, and a window called the Exception Assistant appears indicating where the exception occurred, the type of the exception and links to helpful information on handling the exception. Figure 13.3 shows the Exception Assistant that’s displayed if the user attempts to divide by zero in the application of Fig. 13.1.
When a method called in a program or the CLR detects a problem, the method or the CLR throws an exception. Recall that the point in the program at which an exception occurs is called the throw point—this is an important location for debugging purposes (as we demonstrate in Section 13.7). If an exception occurs in a try
block (such as a FormatException
being thrown as a result of the code in lines 19 and 21 in Fig. 13.2), the try
block terminates immediately, and program control transfers to the first of the following catch
blocks in which the exception parameter’s type matches the type of the thrown exception. In Fig. 13.2, the first catch
block catches FormatException
s (which occur if input of an invalid type is entered); the second catch
block catches DivideByZeroException
s (which occur if an attempt is made to divide by zero). After the exception is handled, program control does not return to the throw point because the try
block has expired (which also causes any of its local variables to go out of scope). Rather, control resumes after the last catch
block. This is known as the termination model of exception handling. [Note: Some languages use the resumption model of exception handling, in which, after an exception is handled, control resumes just after the throw point.]
If no exceptions occur in the try
block, the program of Fig. 13.2 successfully completes the try
block by ignoring the catch
blocks in lines 32–37 and 38–43, and passing line 43. Then the program executes the first statement following the try
and catch
blocks. In this example, the program reaches the end of the do
...while
loop (line 44), so the method terminates, and the program awaits the next user interaction.
The try
block and its corresponding catch
and finally
blocks together form a try
statement. It’s important not to confuse the terms “try
block” and “try
statement”—the term “try
block” refers to the block of code following the keyword try
(but before any catch
or finally
blocks), while the term “try
statement” includes all the code from the opening try
keyword to the end of the last catch
or finally
block. This includes the try
block, as well as any associated catch
blocks and finally
block.
When a try
block terminates, local variables defined in the block go out of scope. If a try
block terminates due to an exception, the CLR searches for the first catch
block that can process the type of exception that occurred. The CLR locates the matching catch
by comparing the type of the thrown exception to each catch
’s parameter type. A match occurs if the types are identical or if the thrown exception’s type is a derived class of the catch
’s parameter type. Once an exception is matched to a catch
block, the code in that block executes and the other catch
blocks in the try
statement are ignored.
In the third sample output of Fig. 13.2, the user inputs hello
as the denominator. When line 21 executes, Convert.ToInt32
cannot convert this string
to an int
, so the method throws a FormatException
object to indicate that the method was unable to convert the string
to an int
. When the exception occurs, the try
block expires (terminates). Next, the CLR attempts to locate a matching catch
block. A match occurs with the catch
block in line 32, so the exception handler displays the exception’s Message
property (to retrieve the error message associated with the exception) and the program ignores all other exception handlers following the try
block. Program control then continues with line 44.
Specifying a comma-separated list of parameters in a catch block is a syntax error. A catch block can have at most one parameter.
In the second sample output of Fig. 13.2, the user inputs 0
as the denominator. When the division in line 25 executes, a DivideByZeroException
occurs. Once again, the try
block terminates, and the program attempts to locate a matching catch
block. In this case, the first catch
block does not match—the exception type in the catch
-handler declaration is not the same as the type of the thrown exception, and FormatException
is not a base class of DivideByZeroException
. Therefore the program continues to search for a matching catch
block, which it finds in line 38. Line 40 displays the exception’s Message
property. Again, program control then continues with line 44.
In C#, the exception-handling mechanism allows only objects of class Exception
(namespace System
) and its derived classes to be thrown and caught. Note, however, that C# programs may interact with software components written in other .NET languages (such as C++) that do not restrict exception types. The general catch
clause can be used to catch such exceptions.
This section overviews several of the .NET Framework’s exception classes and focuses exclusively on exceptions that derive from class Exception
. In addition, we discuss how to determine whether a particular method throws exceptions.
Class Exception
(namespace System
) is the base class of .NET’s exception class hierarchy. An important derived class is SystemException
. The CLR generates SystemException
s. Many of these can be avoided if applications are coded properly. For example, if a program attempts to access an out-of-range array index, the CLR throws an exception of type IndexOutOfRangeException
(a derived class of SystemException
). Similarly, an exception occurs when a program uses a reference-type variable to call a method when the reference has a value of null
. This causes a NullReferenceException
(another derived class of SystemException
). You saw earlier in this chapter that a DivideByZeroException
occurs in integer division when a program attempts to divide by zero.
Other exceptions thrown by the CLR include OutOfMemoryException
, StackOverflowException
and ExecutionEngineException
, which are thrown when something goes wrong that causes the CLR to become unstable. Sometimes such exceptions cannot even be caught. It’s best to simply log such exceptions, then terminate your application.
A benefit of the exception class hierarchy is that a catch
block can catch exceptions of a particular type or—because of the is-a relationship of inheritance—can use a base-class type to catch exceptions in a hierarchy of related exception types. For example, Section 13.3.2 discussed the catch
block with no parameter, which catches exceptions of all types (including those that are not derived from Exception
). A catch
block that specifies a parameter of type Exception
can catch all exceptions that derive from Exception
, because Exception
is the base class of all exception classes. The advantage of this approach is that the exception handler can access the caught exception’s information via the parameter in the catch
. We’ll say more about accessing exception information in Section 13.7.
Using inheritance with exceptions enables an catch
block to catch related exceptions using a concise notation. A set of exception handlers could catch each derived-class exception type individually, but catching the base-class exception type is more concise. However, this technique makes sense only if the handling behavior is the same for a base class and all derived classes. Otherwise, catch each derived-class exception individually.
The compiler issues an error if a catch block that catches a base-class exception is placed before a catch block for any of that class’s derived-class types. In this case, the base-class catch block would catch all base-class and derived-class exceptions, so the derived-class exception handler would never execute.
How do we determine that an exception might occur in a program? For methods contained in the .NET Framework classes, read the detailed descriptions of the methods in the online documentation. If a method throws an exception, its description contains a section called Exceptions that specifies the types of exceptions the method throws and briefly describes what causes them. For an example, search for “Convert.ToInt32
method” in the Visual Studio online documentation. The Exceptions section of this method’s web page indicates that method Convert.ToInt32
throws two exception types—FormatException
and OverflowException
—and describes the reason why each might occur. [Note: You can also find this information in the Object Browser described in Section 10.12.]
If a method throws exceptions, statements that invoke the method directly or indirectly should be placed in try blocks, and those exceptions should be caught and handled.
It’s more difficult to determine when the CLR throws exceptions. Such information appears in the C# Language Specification (available from bit.ly/CSharp4Spec
). This document defines C#’s syntax and specifies cases in which exceptions are thrown.
Programs frequently request and release resources dynamically (i.e., at execution time). For example, a program that reads a file from disk first makes a file-open request (as we’ll see in Chapter 17, Files and Streams). If that request succeeds, the program reads the contents of the file. Operating systems typically prevent more than one program from manipulating a file at once. Therefore, when a program finishes processing a file, the program should close the file (i.e., release the resource) so other programs can use it. If the file is not closed, a resource leak occurs. In such a case, the file resource is not available to other programs, possibly because a program using the file has not closed it.
In programming languages such as C and C++, in which the programmer is responsible for dynamic memory management, the most common type of resource leak is a memory leak. A memory leak occurs when a program allocates memory (as C# programmers do via keyword new
), but does not deallocate the memory when it’s no longer needed. Normally, this is not an issue in C#, because the CLR performs garbage collection of memory that’s no longer needed by an executing program (Section 10.8). However, other kinds of resource leaks (such as unclosed files) can occur.
The CLR does not completely eliminate memory leaks. The CLR will not garbage collect an object until the program contains no more references to that object, and even then there may be a delay until the memory is required. Thus, memory leaks can occur if you inadvertently keep references to unwanted objects.
Typically, exceptions occur when processing resources that require explicit release. For example, a program that processes a file might receive IOException
s during the processing. For this reason, file-processing code normally appears in a try
block. Regardless of whether a program experiences exceptions while processing a file, the program should close the file when it’s no longer needed. Suppose a program places all resource-request and resource-release code in a try
block. If no exceptions occur, the try
block executes normally and releases the resources after using them. However, if an exception occurs, the try
block may expire before the resource-release code can execute. We could duplicate all the resource-release code in each of the catch
blocks, but this would make the code more difficult to modify and maintain. We could also place the resource-release code after the try
statement; however, if the try
block terminated due to a return statement, code following the try
statement would never execute.
To address these problems, C#’s exception-handling mechanism provides the finally
block, which is guaranteed to execute regardless of whether the try
block executes successfully or an exception occurs. This makes the finally
block an ideal location in which to place resource-release code for resources that are acquired and manipulated in the corresponding try
block. If the try
block executes successfully, the finally
block executes immediately after the try
block terminates. If an exception occurs in the try
block, the finally
block executes immediately after a catch
block completes. If the exception is not caught by a catch
block associated with the try
block, or if a catch
block associated with the try
block throws an exception itself, the finally
block executes before the exception is processed by the next enclosing try
block, which could be in the calling method. By placing the resource-release code in a finally
block, we ensure that even if the program terminates due to an uncaught exception, the resource will be deallocated. Local variables in a try
block cannot be accessed in the corresponding finally
block. For this reason, variables that must be accessed in both a try
block, and its corresponding finally
block should be declared before the try
block.
A finally block typically contains code to release resources acquired in the corresponding try block, which makes the finally block an effective mechanism for eliminating resource leaks.
As a rule, resources should be released as soon as they’re no longer needed in a program. This makes them available for reuse promptly.
If one or more catch
blocks follow a try
block, the finally
block is optional. However, if no catch
blocks follow a try
block, a finally
block must appear immediately after the try
block. If any catch
blocks follow a try
block, the finally
block (if there is one) appears after the last catch
block. Only whitespace and comments can separate the blocks in a try
statement.
The application in Fig. 13.4 demonstrates that the finally
block always executes, regardless of whether an exception occurs in the corresponding try
block. The program consists of method Main
(lines 8–47) and four other methods that Main
invokes to demonstrate finally
. These methods are DoesNotThrowException
(lines 50–67), ThrowExceptionWithCatch
(lines 70–89), ThrowExceptionWithoutCatch
(lines 92–108) and ThrowExceptionCatchRethrow
(lines 111–136).
Example 13.4. finally
blocks always execute, even when no exception occurs.
1 // Fig. 13.4: UsingExceptions.cs 2 // Using finally blocks. 3 // finally blocks always execute, even when no exception occurs. 4 using System; 5 6 class UsingExceptions 7 { 8 static void Main() 9 { 10 // Case 1: No exceptions occur in called method 11 Console.WriteLine( "Calling DoesNotThrowException" ); 12 DoesNotThrowException(); 13 14 // Case 2: Exception occurs and is caught in called method 15 Console.WriteLine( " Calling ThrowExceptionWithCatch" ); 16 ThrowExceptionWithCatch(); 17 18 // Case 3: Exception occurs, but is not caught in called method 19 // because there is no catch block. 20 Console.WriteLine( " Calling ThrowExceptionWithoutCatch" ); 21 22 // call ThrowExceptionWithoutCatch 23 try 24 { 25 ThrowExceptionWithoutCatch(); 26 } // end try 27 catch 28 { 29 Console.WriteLine( "Caught exception from " + 30 "ThrowExceptionWithoutCatch in Main" ); 31 } // end catch 32 33 // Case 4: Exception occurs and is caught in called method, 34 // then rethrown to caller. 35 Console.WriteLine( " Calling ThrowExceptionCatchRethrow" ); 36 37 // call ThrowExceptionCatchRethrow 38 try 39 { 40 ThrowExceptionCatchRethrow(); 41 } // end try 42 catch 43 { 44 Console.WriteLine( "Caught exception from " + 45 "ThrowExceptionCatchRethrow in Main" ); 46 } // end catch 47 } // end method Main 48 49 // no exceptions thrown 50 static void DoesNotThrowException() 51 { 52 // try block does not throw any exceptions 53 try 54 { 55 Console.WriteLine( "In DoesNotThrowException" ); 56 } // end try 57 catch 58 { 59 Console.WriteLine( "This catch never executes" ); 60 } // end catch 61 finally 62 { 63 Console.WriteLine( "finally executed in DoesNotThrowException" ); 64 } // end finally 65 66 Console.WriteLine( "End of DoesNotThrowException" ); 67 } // end method DoesNotThrowException 68 69 // throws exception and catches it locally 70 static void ThrowExceptionWithCatch() 71 { 72 // try block throws exception 73 try 74 { 75 Console.WriteLine( "In ThrowExceptionWithCatch" ); 76 throw new Exception( "Exception in ThrowExceptionWithCatch" ); 77 } // end try 78 catch ( Exception exceptionParameter ) 79 { 80 Console.WriteLine( "Message: " + exceptionParameter.Message ); 81 } // end catch 82 finally 83 { 84 Console.WriteLine( 85 "finally executed in ThrowExceptionWithCatch" ); 86 } // end finally 87 88 Console.WriteLine( "End of ThrowExceptionWithCatch" ); 89 } // end method ThrowExceptionWithCatch 90 91 // throws exception and does not catch it locally 92 static void ThrowExceptionWithoutCatch() 93 { 94 // throw exception, but do not catch it 95 try 96 { 97 Console.WriteLine( "In ThrowExceptionWithoutCatch" ); 98 throw new Exception( "Exception in ThrowExceptionWithoutCatch" ); 99 } // end try 100 finally 101 { 102 Console.WriteLine( "finally executed in " + 103 "ThrowExceptionWithoutCatch" ); 104 } // end finally 105 106 // unreachable code; logic error 107 Console.WriteLine( "End of ThrowExceptionWithoutCatch" ); 108 } // end method ThrowExceptionWithoutCatch 109 110 // throws exception, catches it and rethrows it 111 static void ThrowExceptionCatchRethrow() 112 { 113 // try block throws exception 114 try 115 { 116 Console.WriteLine( "In ThrowExceptionCatchRethrow" ); 117 throw new Exception( "Exception in ThrowExceptionCatchRethrow" ); 118 } // end try 119 catch ( Exception exceptionParameter ) 120 { 121 Console.WriteLine( "Message: " + exceptionParameter.Message ); 122 123 // rethrow exception for further processing 124 throw ; 125 126 // unreachable code; logic error 127 } // end catch 128 finally 129 { 130 Console.WriteLine( "finally executed in " + 131 "ThrowExceptionCatchRethrow" ); 132 } // end finally 133 134 // any code placed here is never reached 135 Console.WriteLine( "End of ThrowExceptionCatchRethrow" ); 136 } // end method ThrowExceptionCatchRethrow 137 } // end class UsingExceptions
Calling DoesNotThrowException In DoesNotThrowException finally executed in DoesNotThrowException End of DoesNotThrowException Calling ThrowExceptionWithCatch In ThrowExceptionWithCatch Message: Exception in ThrowExceptionWithCatch finally executed in ThrowExceptionWithCatch End of ThrowExceptionWithCatch Calling ThrowExceptionWithoutCatch In ThrowExceptionWithoutCatch finally executed in ThrowExceptionWithoutCatch Caught exception from ThrowExceptionWithoutCatch in Main Calling ThrowExceptionCatchRethrow In ThrowExceptionCatchRethrow Message: Exception in ThrowExceptionCatchRethrow finally executed in ThrowExceptionCatchRethrow Caught exception from ThrowExceptionCatchRethrow in Main |
Line 12 of Main
invokes method DoesNotThrowException
. This method’s try
block outputs a message (line 55). Because the try
block does not throw any exceptions, program control ignores the catch
block (lines 57–60) and executes the finally
block (lines 61–64), which outputs a message. At this point, program control continues with the first statement after the close of the finally
block (line 66), which outputs a message indicating that the end of the method has been reached. Then, program control returns to Main
.
Line 16 of Main
invokes method ThrowExceptionWithCatch
(lines 70–89), which begins in its try
block (lines 73–77) by outputting a message. Next, the try
block creates an Exception
object and uses a throw
statement to throw it (line 76). Executing the throw
statement indicates that a problem has occurred in the code. As you’ve seen in earlier chapters, you can throw exceptions by using the throw
statement. Just as with exceptions thrown by the Framework Class Library’s methods and the CLR, this indicates to client applications that an error has occurred. A throw
statement specifies an object to be thrown. The operand of a throw
statement can be of type Exception
or of any type derived from class Exception
.
The string
passed to the constructor becomes the exception object’s error message. When a throw
statement in a try
block executes, the try
block expires immediately, and program control continues with the first matching catch
block (lines 78–81) following the try
block. In this example, the type thrown (Exception
) matches the type specified in the catch
, so line 80 outputs a message indicating the exception that occurred. Then, the finally
block (lines 82–86) executes and outputs a message. At this point, program control continues with the first statement after the close of the finally
block (line 88), which outputs a message indicating that the end of the method has been reached. Program control then returns to Main
. In line 80, we use the exception object’s Message
property to retrieve the error message associated with the exception (i.e., the message passed to the Exception
constructor). Section 13.7 discusses several properties of class Exception
.
Lines 23–31 of Main
define a try
statement in which Main
invokes method ThrowExceptionWithoutCatch
(lines 92–108). The try
block enables Main
to catch any exceptions thrown by ThrowExceptionWithoutCatch
. The try
block in lines 95–99 of ThrowExceptionWithoutCatch
begins by outputting a message. Next, the try
block throws an Exception
(line 98) and expires immediately.
Normally, program control would continue at the first catch
following this try
block. However, this try
block does not have any catch
blocks. Therefore, the exception is not caught in method ThrowExceptionWithoutCatch
. Program control proceeds to the finally
block (lines 100–104), which outputs a message. At this point, program control returns to Main
—any statements appearing after the finally
block (e.g., line 107) do not execute. In this example, such statements could cause logic errors, because the exception thrown in line 98 is not caught. In Main
, the catch
block in lines 27–31 catches the exception and displays a message indicating that the exception was caught in Main
.
Lines 38–46 of Main
define a try
statement in which Main
invokes method ThrowExceptionCatchRethrow
(lines 111–136). The try
statement enables Main
to catch any exceptions thrown by ThrowExceptionCatchRethrow
. The try
statement in lines 114–132 of ThrowExceptionCatchRethrow
begins by outputting a message. Next, the try
block throws an Exception
(line 117). The try
block expires immediately, and program control continues at the first catch
(lines 119–127) following the try
block. In this example, the type thrown (Exception
) matches the type specified in the catch
, so line 121 outputs a message indicating where the exception occurred. Line 124 uses the throw
statement to rethrow the exception. This indicates that the catch
block performed partial processing of the exception and now is throwing the exception again (in this case, back to the method Main
) for further processing. In general, it’s considered better practice to throw a new exception and pass the original one to the new exception’s constructor. This maintains all of the stack-trace information from the original exception. Rethrowing an exception loses the original exception’s stack-trace information.
You can also rethrow an exception with a version of the throw
statement which takes an operand that is the reference to the exception that was caught. It’s important to note, however, that this form of throw
statement resets the throw point, so the original throw point’s stack-trace information is lost. Section 13.7 demonstrates using a throw
statement with an operand from a catch
block. In that section, you’ll see that after an exception is caught, you can create and throw a different type of exception object from the catch
block and you can include the original exception as part of the new exception object. Class library designers often do this to customize the exception types thrown from methods in their class libraries or to provide additional debugging information.
The exception handling in method ThrowExceptionCatchRethrow
does not complete, because the throw
statement in line 124 immediately terminates the catch
block—if there were any code between line 124 and the end of the block, it would not execute. When line 124 executes, method ThrowExceptionCatchRethrow
terminates and returns control to Main
. Once again, the finally
block (lines 128–132) executes and outputs a message before control returns to Main
. When control returns to Main
, the catch
block in lines 42–46 catches the exception and displays a message indicating that the exception was caught. Then the program terminates.
The next statement to execute after a finally
block terminates depends on the exception-handling state. If the try
block successfully completes, or if a catch
block catches and handles an exception, the program continues its execution with the next statement after the finally
block. However, if an exception is not caught, or if a catch
block rethrows an exception, program control continues in the next enclosing try
block. The enclosing try
could be in the calling method or in one of its callers. It also is possible to nest a try
statement in a try
block; in such a case, the outer try
statement’s catch
blocks would process any exceptions that were not caught in the inner try
statement. If a try
block executes and has a corresponding finally
block, the finally
block executes even if the try
block terminates due to a return
statement. The return
occurs after the execution of the finally
block.
If an uncaught exception is awaiting processing when the finally block executes, and the finally block throws a new exception that’s not caught in the finally block, the first exception is lost, and the new exception is passed to the next enclosing try block.
When placing code that can throw an exception in a finally block, always enclose the code in a try statement that catches the appropriate exception types. This prevents the loss of any uncaught and rethrown exceptions that occur before the finally block executes.
Do not place try blocks around every statement that might throw an exception—this can make programs difficult to read. Instead, place one try block around a significant portion of code, and follow this try block with catch blocks that handle each possible exception. Then follow the catch blocks with a single finally block. Use separate try blocks to distinguish between multiple statements that can throw the same exception type.
Typically resource-release code should be placed in a finally
block to ensure that a resource is released, regardless of whether there were exceptions when the resource was used in the corresponding try
block. An alternative notation—the using
statement (not to be confused with the using
directive for using namespaces)—simplifies writing code in which you obtain a resource, use the resource in a try
block and release the resource in a corresponding finally
block. For example, a file-processing application (Chapter 17) could process a file with a using
statement to ensure that the file is closed properly when it’s no longer needed. The resource must be an object that implements the IDisposable
interface and therefore has a Dispose
method. The general form of a using
statement is
using ( ExampleObject exampleObject = new ExampleObject() ) { exampleObject.SomeMethod(); } |
where ExampleObject
is a class that implements the IDisposable
interface. This code creates an object of type ExampleObject
and uses it in a statement, then calls its Dispose
method to release any resources used by the object. The using
statement implicitly places the code in its body in a try
block with a corresponding finally
block that calls the object’s Dispose
method. For instance, the preceding code is equivalent to
{ ExampleObject exampleObject = new ExampleObject(); try { exampleObject.SomeMethod(); } finally { if ( exampleObject != null ) ( ( IDisposable ) exampleObject ).Dispose(); } } |
The if
statement ensures that exampleObject
still references an object; otherwise, a NullReferenceException
might occur.
As we discussed in Section 13.4, exception types derive from class Exception
, which has several properties. These frequently are used to formulate error messages indicating a caught exception. Two important properties are Message
and StackTrace
. Property Message
stores the error message associated with an Exception
object. This message can be a default message associated with the exception type or a customized message passed to an Exception
object’s constructor when the Exception
object is thrown. Property StackTrace
contains a string
that represents the method-call stack. Recall that the runtime environment at all times keeps a list of open method calls that have been made but have not yet returned. The StackTrace
represents the series of methods that have not finished processing at the time the exception occurs. If the debugging information that is generated by the compiler for the method is accessible to the IDE, the stack trace also includes line numbers; the first line number indicates the throw point, and subsequent line numbers indicate the locations from which the methods in the stack trace were called. PDB files are created by the IDE to maintain the debugging information for your projects.
Another property used frequently by class-library programmers is InnerException
. Typically, class library programmers “wrap” exception objects caught in their code so that they then can throw new exception types that are specific to their libraries. For example, a programmer implementing an accounting system might have some account-number processing code in which account numbers are input as string
s but represented as int
s in the code. Recall that a program can convert string
s to int
values with Convert.ToInt32
, which throws a FormatException
when it encounters an invalid number format. When an invalid account-number format occurs, the accounting-system programmer might wish to employ a different error message than the default message supplied by FormatException
or might wish to indicate a new exception type, such as InvalidAccountNumberFormatException
. In such cases, you would provide code to catch the FormatException
, then create an appropriate type of Exception
object in the catch
block and pass the original exception as one of the constructor arguments. The original exception object becomes the InnerException
of the new exception object. When an InvalidAccountNumberFormatException
occurs in code that uses the accounting system library, the catch
block that catches the exception can obtain a reference to the original exception via property InnerException
. Thus the exception indicates both that the user specified an invalid account number and that the problem was an invalid number format. If the InnerException
property is null
, this indicates that the exception was not caused by another exception.
Class Exception
provides other properties, including HelpLink
, Source
and TargetSite
. Property HelpLink
specifies the location of the help file that describes the problem that occurred. This property is null
if no such file exists. Property Source
specifies the name of the application or object that caused the exception. Property TargetSite
specifies the method where the exception originated.
Our next example (Fig. 13.5) demonstrates properties Message
, StackTrace
and InnerException
of class Exception
. In addition, the example introduces stack unwinding—when an exception is thrown but not caught in a particular scope, the method-call stack is “unwound,” and an attempt is made to catch the exception in the next outer try
block. We keep track of the methods on the call stack as we discuss property StackTrace
and the stack-unwinding mechanism. To see the proper stack trace, you should execute this program using steps similar to those presented in Section 13.2.
Example 13.5. Stack unwinding and Exception
class properties.
1 // Fig. 13.5: Properties.cs 2 // Stack unwinding and Exception class properties. 3 // Demonstrates using properties Message, StackTrace and InnerException. 4 using System; 5 6 class Properties 7 { 8 static void Main() 9 { 10 // call Method1; any Exception generated is caught 11 // in the catch block that follows 12 try 13 { 14 Method1(); 15 } // end try 16 catch ( Exception exceptionParameter ) 17 { 18 // output the string representation of the Exception, then output 19 // properties Message, StackTrace and InnerException 20 Console.WriteLine( "exceptionParameter.ToString: {0} ", 21 exceptionParameter ); 22 Console.WriteLine( "exceptionParameter.Message: {0} ", 23 exceptionParameter.Message ); 24 Console.WriteLine( "exceptionParameter.StackTrace: {0} ", 25 exceptionParameter.StackTrace ); 26 Console.WriteLine( "exceptionParameter.InnerException: {0} ", 27 exceptionParameter.InnerException ); 28 } // end catch 29 } // end method Main 30 31 // calls Method2 32 static void Method1() 33 { 34 Method2(); 35 } // end method Method1 36 37 // calls Method3 38 static void Method2() 39 { 40 Method3(); 41 } // end method Method2 42 43 // throws an Exception containing an InnerException 44 static void Method3() 45 { 46 // attempt to convert string to int 47 try 48 { 49 Convert.ToInt32( "Not an integer" ); 50 } // end try 51 catch ( FormatException formatExceptionParameter ) 52 { 53 // wrap FormatException in new Exception 54 throw new Exception( "Exception occurred in Method3" , 55 formatExceptionParameter ); 56 } // end catch 57 } // end method Method3 58 } // end class Properties
exceptionParameter.ToString: System.Exception: Exception occurred in Method3 ---> System.FormatException: Input string was not in a correct format. |
at System.Number.StringToNumber(String str, NumberStyles options, NumberBuffer& number, NumberFormatInfo info, Boolean parseDecimal) at System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo info) at Properties.Method3() in C:examplesch13Fig13_05Properties PropertiesProperties.cs:line 49 --- End of inner exception stack trace --- at Properties.Method3() in C:examplesch13Fig13_05Properties PropertiesProperties.cs:line 54 at Properties.Method2() in C:examplesch13Fig13_05Properties PropertiesProperties.cs:line 40 at Properties.Method1() in C:examplesch13Fig13_05Properties PropertiesProperties.cs:line 34 at Properties.Main() in C:examplesch13Fig13_05Properties PropertiesProperties.cs:line 14 exceptionParameter.Message: Exception occurred in Method3 exceptionParameter.StackTrace: at Properties.Method3() in C:examplesch13Fig13_05Properties PropertiesProperties.cs: line 54 at Properties.Method2() in C:examplesch13Fig13_05Properties PropertiesProperties.cs: line 40 at Properties.Method1() in C:examplesch13Fig13_05Properties PropertiesProperties.cs: line 34 at Properties.Main() in C:examplesch13Fig13_05Properties PropertiesProperties.cs: line 14 exceptionParameter.InnerException: System.FormatException: Input string was not in a correct format. at System.Number.StringToNumber(String str, NumberStyles options, NumberBuffer& number, NumberFormatInfo info, Boolean parseDecimal) at System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo info) at Properties.Method3() in C:examplesch13Fig13_05Properties PropertiesProperties.cs: line 49 |
Program execution begins with Main
, which becomes the first method on the method-call stack. Line 14 of the try
block in Main
invokes Method1
(declared in lines 32–35), which becomes the second method on the stack. If Method1
throws an exception, the catch
block in lines 16–28 handles the exception and outputs information about the exception that occurred. Line 34 of Method1
invokes Method2
(lines 38–41), which becomes the third method on the stack. Then line 40 of Method2
invokes Method3
(lines 44–57), which becomes the fourth method on the stack.
At this point, the method-call stack (from top to bottom) for the program is:
Method3 Method2 Method1 Main |
The method called most recently (Method3
) appears at the top of the stack; the first method called (Main
) appears at the bottom. The try
statement (lines 47–56) in Method3
invokes method Convert.ToInt32
(line 49), which attempts to convert a string
to an int
. At this point, Convert.ToInt32
becomes the fifth and final method on the call stack.
Because the argument to Convert.ToInt32
is not in int
format, line 49 throws a FormatException
that’s caught in line 51 of Method3
. The exception terminates the call to Convert.ToInt32
, so the method is removed (or unwound) from the method-call stack. The catch
block in Method3
then creates and throws an Exception
object. The first argument to the Exception
constructor is the custom error message for our example, “Exception occurred in Method3
.” The second argument is the InnerException
—the FormatException
that was caught. The StackTrace
for this new exception object reflects the point at which the exception was thrown (lines 54–55). Now Method3
terminates, because the exception thrown in the catch
block is not caught in the method body. Thus, control returns to the statement that invoked Method3
in the prior method in the call stack (Method2
). This removes, or unwinds, Method3
from the method-call stack.
When control returns to line 40 in Method2
, the CLR determines that line 40 is not in a try
block. Therefore the exception cannot be caught in Method2
, and Method2
terminates. This unwinds Method2
from the call stack and returns control to line 34 in Method1
.
Here again, line 34 is not in a try
block, so Method1
cannot catch the exception. The method terminates and is unwound from the call stack, returning control to line 14 in Main
, which is located in a try
block. The try
block in Main
expires and the catch
block (lines 16–28) catches the exception. The catch
block uses properties Message
, StackTrace
and InnerException
to create the output. Stack unwinding continues until a catch
block catches the exception or the program terminates.
The first block of output (which we reformatted for readability) in Fig. 13.5 contains the exception’s string
representation, which is returned from an implicit call to method ToString
. The string
begins with the name of the exception class followed by the Message
property value. The next four items present the stack trace of the InnerException
object. The remainder of the block of output shows the StackTrace
for the exception thrown in Method3
. The StackTrace
represents the state of the method-call stack at the throw point of the exception, rather than at the point where the exception eventually is caught. Each StackTrace
line that begins with “at
” represents a method on the call stack. These lines indicate the method in which the exception occurred, the file in which the method resides and the line number of the throw point in the file. The inner-exception information includes the inner-exception stack trace.
When catching and rethrowing an exception, provide additional debugging information in the rethrown exception. To do so, create an Exception object containing more specific debugging information, then pass the original caught exception to the new exception object’s constructor to initialize the InnerException property.
The next block of output (two lines) simply displays the Message
property’s value (Exception occurred in Method3
) of the exception thrown in Method3
.
The third block of output displays the StackTrace
property of the exception thrown in Method3
. This StackTrace
property contains the stack trace starting from line 54 in Method3
, because that’s the point at which the Exception
object was created and thrown. The stack trace always begins from the exception’s throw point.
Finally, the last block of output displays the string
representation of the InnerException
property, which includes the namespace and class name of the exception object, as well as its Message
and StackTrace
properties.
In many cases, you can use existing exception classes from the .NET Framework Class Library to indicate exceptions that occur in your programs. In some cases, however, you might wish to create new exception classes specific to the problems that occur in your programs. User-defined exception classes should derive directly or indirectly from class Exception
of namespace System
. When you create code that throws exceptions, they should be well documented, so that other developers who use your code will know how to handle them.
Associating each type of malfunction with an appropriately named exception class improves program clarity.
Before creating a user-defined exception class, investigate the existing exceptions in the .NET Framework Class Library to determine whether an appropriate exception type already exists.
Figures 13.6–13.7 demonstrate a user-defined exception class. NegativeNumberException
(Fig. 13.6) represents exceptions that occur when a program performs an illegal operation on a negative number, such as attempting to calculate its square root.
Example 13.6. NegativeNumberException
represents exceptions caused by illegal operations performed on negative numbers.
1 // Fig. 13.6: NegativeNumberException.cs 2 // NegativeNumberException represents exceptions caused by 3 // illegal operations performed on negative numbers. 4 using System; 5 6 class NegativeNumberException : Exception 7 { 8 // default constructor 9 public NegativeNumberException() 10 : base( "Illegal operation for a negative number" ) 11 { 12 // empty body 13 } // end default constructor 14 15 // constructor for customizing error message 16 public NegativeNumberException( string messageValue ) 17 : base( messageValue ) 18 { 19 // empty body 20 } // end one-argument constructor 21 22 // constructor for customizing the exception's error 23 // message and specifying the InnerException object 24 public NegativeNumberException( string messageValue, 25 Exception inner ) 26 : base( messageValue, inner ) 27 { 28 // empty body 29 } // end two-argument constructor 30 } // end namespace SquareRootTest
Example 13.7. Demonstrating a user-defined exception class.
1 // Fig. 13.7: SquareRootTest.cs 2 // Demonstrating a user-defined exception class. 3 using System; 4 5 class SquareRootTest 6 { 7 static void Main( string [] args ) 8 { 9 bool continueLoop = true ; 10 11 do 12 { 13 // catch any NegativeNumberException thrown 14 try 15 { 16 Console.Write( 17 "Enter a value to calculate the square root of: " ); 18 double inputValue = Convert.ToDouble( Console.ReadLine() ); 19 double result = SquareRoot( inputValue ); 20 21 Console.WriteLine( "The square root of {0} is {1:F6} ", 22 inputValue, result ); 23 continueLoop = false; 24 } // end try 25 catch ( FormatException formatException ) 26 { 27 Console.WriteLine( " " + formatException.Message ); 28 Console.WriteLine( "Please enter a double value. " ); 29 } // end catch 30 catch ( NegativeNumberException negativeNumberException ) 31 { 32 Console.WriteLine( " " + negativeNumberException.Message ); 33 Console.WriteLine( "Please enter a non-negative value. " ); 34 } // end catch 35 } while ( continueLoop ); 36 } // end Main 37 38 // computes square root of parameter; throws 39 // NegativeNumberException if parameter is negative 40 public static double SquareRoot( double value ) 41 { 42 // if negative operand, throw NegativeNumberException 43 if ( value < 0 ) 44 throw new NegativeNumberException( 45 "Square root of negative number not permitted" ); 46 else 47 return Math.Sqrt( value ); // compute square root 48 } // end method SquareRoot 49 } // end class SquareRootTest
Enter a value to calculate the square root of: 30 The square root of 30 is 5.477226 Enter a value to calculate the square root of: hello Input string was not in a correct format. Please enter a double value. Enter a value to calculate the square root of: 25 The square root of 25 is 5.000000 |
Enter a value to calculate the square root of: -2 Square root of negative number not permitted Please enter a non-negative value. Enter a value to calculate the square root of: 2 The square root of 2 is 1.414214 |
According to Microsoft’s docuemtn on “Best Practices for Handling Exceptions” (bit.ly/ExceptionsBestPractices
), user-defined exceptions should typically extend class Exception
, have a class name that ends with “Exception” and define three constructors: a parameterless constructor; a constructor that receives a string
argument (the error message); and a constructor that receives a string
argument and an Exception
argument (the error message and the inner-exception object). Defining these three constructors makes your exception class more flexible, allowing other programmers to easily use and extend it.
NegativeNumberException
s most frequently occur during arithmetic operations, so it seems logical to derive class NegativeNumberException
from class ArithmeticException
. However, class ArithmeticException
derives from class SystemException
—the category of exceptions thrown by the CLR. Per Microsoft’s best practices for exception handling, user-defined exception classes should inherit from Exception
rather than SystemException
. In this case, we could have used the built-in ArgumentException
class, which is recommended in the best practices for invalid argument values. We create our own exception type here simply for demonstration purposes.
Class SquareRootTest
(Fig. 13.7) demonstrates our user-defined exception class. The application enables the user to input a numeric value, then invokes method SquareRoot
(lines 40–48) to calculate the square root of that value. To perform this calculation, SquareRoot
invokes class Math
’s Sqrt
method, which receives a double
value as its argument. Normally, if the argument is negative, method Sqrt
returns NaN
. In this program, we’d like to prevent the user from calculating the square root of a negative number. If the numeric value that the user enters is negative, method SquareRoot
throws a NegativeNumberException
(lines 44–45). Otherwise, SquareRoot
invokes class Math
’s method Sqrt
to compute the square root (line 47).
When the user inputs a value, the try
statement (lines 14–34) attempts to invoke SquareRoot
using the value input by the user. If the user input is not a number, a FormatException
occurs, and the catch
block in lines 25–29 processes the exception. If the user inputs a negative number, method SquareRoot
throws a NegativeNumberException
(lines 44–45); the catch
block in lines 30–34 catches and handles this type of exception.
In this chapter, you learned how to use exception handling to deal with errors in an application. We demonstrated that exception handling enables you to remove error-handling code from the “main line” of the program’s execution. You saw exception handling in the context of a divide-by-zero example. You learned how to use try
blocks to enclose code that may throw an exception, and how to use catch
blocks to deal with exceptions that may arise. We explained the termination model of exception handling, in which, after an exception is handled, program control does not return to the throw point. We discussed several important classes of the .NET Exception
hierarchy, including Exception
(from which user-defined exception classes are derived) and SystemException
. Next you learned how to use the finally
block to release resources whether or not an exception occurs, and how to throw and rethrow exceptions with the throw
statement. We showed how the using
statement can be used to automate the process of releasing a resource. You then learned how to obtain information about an exception using Exception
properties Message
, StackTrace
and InnerException
, and method ToString
. You learned how to create your own exception classes. In the next two chapters, we present an in-depth treatment of graphical user interfaces. In these chapters and throughout the rest of the book, we use exception handling to make our examples more robust, while demonstrating new features of the language.
An exception is an indication of a problem that occurs during a program’s execution.
Exception handling enables you to create applications that can resolve (or handle) exceptions.
An exception is thrown when a method or the CLR detects a problem and is unable to handle it.
A stack trace includes the name of the exception in a descriptive message that indicates the problem that occurred and the complete method-call stack at the time the exception occurred.
Division by zero is not allowed in integer arithmetic.
Division by zero is allowed with floating-point values. Such a calculation results in the value infinity, which is represented by Double.PositiveInfinity
or Double.NegativeInfinity
, depending on whether the numerator is positive or negative. If both the numerator and denominator are zero, the result of the calculation is Double.NaN
.
When division by zero occurs in integer arithmetic, a DivideByZeroException
is thrown.
A FormatException
occurs when Convert
method ToInt32
receives a string that does not represent a valid integer.
A try
block encloses the code that might throw exceptions, as well as the code that should not execute if an exception occurs.
A catch
block can specify an identifier representing the exception that the catch
block can handle. A general catch
clause catches all exception types, but cannot access exception information.
At least one catch
block and/or a finally
block must immediately follow the try
block.
An uncaught exception is an exception that occurs for which there’s no matching catch
block.
When a method called in a program detects an exception, or when the CLR detects a problem, the method or the CLR throws an exception.
The point in the program at which an exception occurs is called the throw point.
If an exception occurs in a try
block, the try
block terminates immediately, and program control transfers to the first of the following catch
blocks in which the exception parameter’s type matches the type of the thrown exception.
After an exception is handled, program control does not return to the throw point, because the try
block has expired. Instead, control resumes after the try
statement’s last catch
block. This is known as the termination model of exception handling.
The try
block and its corresponding catch
and finally
blocks together form a try
statement.
The CLR locates the matching catch
by comparing the thrown exception’s type to each catch
’s exception-parameter type. A match occurs if the types are identical or if the thrown exception’s type is a derived class of the exception-parameter type.
Once an exception is matched to a catch
block, the other catch
blocks are ignored.
The C# exception-handling mechanism allows objects only of class Exception
and its derived classes to be thrown and caught.
Class Exception
of namespace System
is the base class of the .NET Framework Class Library exception class hierarchy.
The CLR generates SystemExceptions
, which can occur at any point during the execution of the program. Many of these exceptions can be avoided if applications are coded properly.
A benefit of using the exception class hierarchy is that a catch
block can catch exceptions of a particular type or—because of the is-a relationship of inheritance—can use a base-class type to catch exceptions in a hierarchy of related exception types.
A catch
block that specifies an exception parameter of type Exception
can catch all exceptions that derive from Exception
, because Exception
is the base class of all exception classes.
Using inheritance with exceptions enables an exception handler to catch related exceptions.
The most common type of resource leak is a memory leak.
A memory leak occurs when a program allocates memory but does not deallocate it when it’s no longer needed. Normally, this is not an issue in C#, because the CLR performs garbage collection of memory that’s no longer needed by an executing program.
C#’s exception-handling mechanism provides the finally
block, which is guaranteed to execute if program control enters the corresponding try
block.
The finally
block executes regardless of whether the corresponding try
block executes successfully or an exception occurs. This makes the finally
block an ideal location in which to place resource-release code for resources acquired and manipulated in the corresponding try
block.
If a try
block executes successfully, the finally
block executes immediately after the try
block terminates. If an exception occurs in the try
block, the finally
block executes immediately after a catch
block completes.
If the exception is not caught by a catch
block associated with the try
block, or if a catch
block associated with the try
block throws an exception, the finally
block executes before the exception is processed by the next enclosing try
block (if there is one).
A throw
statement can rethrow an exception, indicating that a catch
block performed partial processing of the exception and now is throwing the exception again for further processing.
If a try
block executes and has a corresponding finally
block, the finally
block always executes. The return
occurs after the execution of the finally
block.
The using
statement simplifies writing code in which you obtain a resource, use the resource in a try
block and release the resource in a corresponding finally
block.
Property Message
of class Exception
stores the error message associated with an Exception
object.
Property StackTrace
of class Exception
contains a string
that represents the method-call stack.
Another Exception
property used frequently by class library programmers is InnerException
. Typically, you use this property to “wrap” exception objects caught in your code, so that you then can throw new exception types specific to your libraries.
When an exception is thrown but not caught in a particular scope, stack unwinding occurs and an attempt is made to catch the exception in the next outer try
block.
User-defined exception classes should derive directly or indirectly from class Exception
of namespace System
.
User-defined exceptions should typically extend Exception
, have a class name that ends with “Exception” and define a parameterless constructor, a constructor that receives a string
argument (the error message), and a constructor that receives a string
argument and an Exception
argument (the error message and the inner-exception object).
[1] “Best Practices for Handling Exceptions [C#],” .NET Framework Developer’s Guide, Visual Studio .NET Online Help. Available at msdn.microsoft.com/en-us/library/seyhszts.aspx