In VB.NET, all exceptions will be either of type System.Exception or of types derived from System.Exception. The CLR System namespace includes a number of exception types that can be used by your program. These exception types include ArgumentNullException, InvalidCastException, and OverflowException, as well as many others. You can guess their use based on their name. For example, ArgumentNullException is thrown when an argument to a method is Nothing (null) and that is not an expected (or acceptable) value.
This chapter describes how to write your programs to catch and handle exceptions. This chapter will also show you how to use the properties of the Exception class to provide information to the user about what went wrong, and it will show you how to create and use your own custom exception types.
To signal an abnormal condition in a VB.NET program, you throw an exception. To do this, you use the keyword Throw. The following line of code creates a new instance of System.Exception and then throws it:
Throw New System.Exception( )
Example 11-1 illustrates what happens if you throw an exception and there is no try/catch block to catch and handle the exception. In this example, you'll throw an exception even though nothing has actually gone wrong, just to illustrate how an exception can bring your program to a halt.
Option Strict On Imports System Namespace ExceptionHandling Class Tester Shared Sub Main( ) Console.WriteLine("Enter Main...") Dim t As New Tester( ) t.Run( ) Console.WriteLine("Exit Main...") End Sub 'Main Public Sub Run( ) Console.WriteLine("Enter Run...") Func1( ) Console.WriteLine("Exit Run...") End Sub 'Run Public Sub Func1( ) Console.WriteLine("Enter Func1...") Func2( ) Console.WriteLine("Exit Func1...") End Sub 'Func1 Public Sub Func2( ) Console.WriteLine("Enter Func2...") Throw New System.Exception( ) Console.WriteLine("Exit Func2...") End Sub 'Func2 End Class 'Tester End Namespace 'ExceptionHandling Enter Main... Enter Run... Enter Func1... Enter Func2... Unhandled Exception: System.Exception: Exception of type System.Exception was thrown. at DebuggingVB.ExceptionHandling.Tester.Func2( ) in C:...\Module1.vb:line 27 at DebuggingVB.ExceptionHandling.Tester.Func1( ) in C:...\Module1.vb:line 21 at DebuggingVB.ExceptionHandling.Tester.Run( ) in C:...\Module1.vb:line 14 at DebuggingVB.ExceptionHandling.Tester.Main( ) in C:...\Module1.vb:line 8
This simple example writes to the console as it enters and exits each method. Main( ) calls Run, which in turn calls Func1( ). After printing out the Enter Func1 message, Func1( ) immediately calls Func2( ). Func2( ) prints out the first message and throws an object of type System.Exception.
Execution immediately stops, and the CLR looks to see if there is a handler in Func2( ). There is not, and so the runtime unwinds the stack (never printing the exit statement) to Func1( ). Again, there is no handler, and the runtime unwinds the stack back to Main( ). With no exception handler there, the default handler is called, which prints the error message.
To handle exceptions you take the following steps:
Execute any code that you suspect might throw an exception (such as code that opens a file or allocates memory) in a try block.
Catch any exceptions that are thrown in a catch block.
A try block is created with the keyword Try, and is ended with the keywords End Try. A catch block is created using the keyword Catch. A catch block can be terminated either by the next use of the Catch keyword or by the End Try statement. Example 11-2 illustrates these constructs. Note that Example 11-2 is the same as Example 11-1 except that a try/catch block has been added.
Option Strict On Imports System Namespace ExceptionHandling Class Tester Shared Sub Main( ) Console.WriteLine("Enter Main...") Dim t As New Tester( ) t.Run( ) Console.WriteLine("Exit Main...") End Sub 'Main Public Sub Run( ) Console.WriteLine("Enter Run...") Func1( ) Console.WriteLine("Exit Run...") End Sub 'Run Public Sub Func1( ) Console.WriteLine("Enter Func1...") Func2( ) Console.WriteLine("Exit Func1...") End Sub 'Func1 Public Sub Func2( ) Console.WriteLine("Enter Func2...") Try Console.WriteLine("Entering Try block...") Throw New System.Exception( ) Console.WriteLine("Exitintg Try block...") Catch Console.WriteLine("Exception caught and handled") End Try Console.WriteLine("Exit func2...") End Sub 'Func2 End Class 'Tester End Namespace 'ExceptionHandling Output: Enter Main... Enter Run... Enter Func1... Enter Func2... Entering try block... Exception caught and handled! Exit Func2... Exit Func1... Exit Run... Exit Main...
Following the Try statement in Example 11-2 is a generic Catch statement. The Catch statement is generic because you haven't specified what kind of exceptions to catch. If you don't specify a particular exception type, the catch block will catch any exceptions that are thrown. Using Catch statements to catch specific types of exceptions is discussed later in this chapter.
Notice that the Exit Func* statements are now written. With the exception handled, execution resumes immediately after the catch statement.
In Example 11-2, the Catch statement simply reports that the exception has been caught and handled. In a real Catch statement, you might take corrective action to fix the problem that caused an exception to be thrown. For example, if the user is trying to open a read-only file, you might invoke a method that allows the user to change the attributes of the file. If the program has run out of memory, you might give the user an opportunity to close other applications. If all else fails, the catch block can print an error message so that the user knows what went wrong.
Examine the output of Example 11-2 carefully. You see the code enter Main( ), Func1( ), Func2( ), and the try block. You never see it exit the try block, though it does exit Func2(), Func1( ), and Main( ). What happened?
When the exception is thrown, execution halts immediately and is handed to the catch block. It never returns to the original code path. It never gets to the line that prints the Exit statement for the try block. The catch block handles the error, and then execution falls through to the code following the catch block.
Because there is a catch block, the stack does not need to unwind. The exception is now handled; there are no more problems, and the program continues. This becomes a bit clearer if you move the try/catch blocks up to Func1( ), as shown in Example 11-3.
Option Strict On Imports System Namespace ExceptionHandling Class Tester Shared Sub Main( ) Console.WriteLine("Enter Main...") Dim t As New Tester( ) t.Run( ) Console.WriteLine("Exit Main...") End Sub 'Main Public Sub Run( ) Console.WriteLine("Enter Run...") Func1( ) Console.WriteLine("Exit Run...") End Sub 'Run Public Sub Func1( ) Console.WriteLine("Enter func1...") Try Console.WriteLine("Entering Try block...") Func2( ) Console.WriteLine("Exiting Try block...") Catch Console.WriteLine("Exception caught and handled") End Try Console.WriteLine("Exit func1...") End Sub 'Func1 Public Sub Func2( ) Console.WriteLine("Enter Func2...") Throw New System.Exception( ) Console.WriteLine("Exit Func2...") End Sub 'Func2 End Class 'Tester End Namespace 'ExceptionHandling Output: Enter Main... Enter Run... Enter Func1... Entering try block... Enter Func2... Exception caught and handled! Exit Func1... Exit Run... Exit Main...
This time the exception is not handled in Func2( ); it is handled in Func1( ). When Func2( ) is called, it uses Console.WriteLine( ) to display its first milestone:
Enter Func2...
Then Func2( ) throws an exception and execution halts. The runtime looks for a handler in Func2( ), but there isn't one. Then the stack begins to unwind, and the runtime looks for a handler in the calling function, Func1( ). There is a catch block in Func1( ) so its code is executed; then execution resumes immediately following the Catch statement, printing the Exit statement for Func1( ) and then for Main( ).
If you're not entirely sure with why the Exiting Try Block statement and the Exit Func2 statement are not printed, try putting the code into a debugger and then stepping through it.
So far, you've been working only with generic Catch statements. You can create dedicated Catch statements that handle only some exceptions and not others, based on the type of exception thrown. Example 11-4 illustrates how to specify which exception you'd like to handle.
Option Strict On Imports System Namespace ExceptionHandling Class Tester Public Sub Run( ) Try Dim a As Double = 5 Dim b As Double = 0 Console.WriteLine("Dividing {0} by {1}...", a, b) Console.WriteLine("{0} / {1} = {2}", _ a, b, DoDivide(a, b)) ' most derived exception type first Catch e As System.DivideByZeroException Console.WriteLine("DivideByZeroException caught!") Catch e As System.ArithmeticException Console.WriteLine("ArithmeticException caught!") ' generic exception type last Catch Console.WriteLine("Unknown exception caught") End Try End Sub ' do the division if legal Public Function DoDivide(ByVal a As Double, ByVal b As Double) As Double If b = 0 Then Throw New System.DivideByZeroException( ) End If If a = 0 Then Throw New System.ArithmeticException( ) End If Return a / b End Function Public Shared Sub Main( ) Console.WriteLine("Enter Main...") Dim t As Tester = New Tester( ) t.Run( ) Console.WriteLine("Exit Main...") End Sub End Class End Namespace Output: Enter Main... Dividing 5 by 0... DivideByZeroException caught! Exit Main...
In Example 11-4, the DoDivide( ) method will not let you divide zero by another number, nor will it let you divide a number by zero. If you try to divide by zero, it throws an instance of DivideByZeroException. If you try to divide zero by another number, there is no appropriate exception: dividing zero by another number is a legal mathematical operation and shouldn't throw an exception at all. However, for the sake of this example, assume you don't want to allow division of zero by any number; you will throw an ArithmeticException.
When the exception is thrown, the runtime examines each exception handler in the order in which they appear in the code and matches the first one it can. When you run this with a=5 and b=7, the output is:
5 / 7 = 0.7142857142857143
As you'd expect, no exception is thrown. However, when you change the value of a to 0, the output is:
ArithmeticException caught!
The exception is thrown, and the runtime examines the first exception, DivideByZeroException. Because this does not match, it goes on to the next handler, ArithmeticException, which does match.
In a final pass through, suppose you change a to 7 and b to 0. This throws the DivideByZeroException.
|
Typically, a method will catch every exception it can anticipate for the code it is running. However, it is possible to distribute your try/catch statements, catching some specific exceptions in one function and more generic exceptions in higher, calling functions. Your design goals should dictate the exact design.
Assume you have a Method A that calls another Method B, which in turn calls Method C, which calls Method D, which then calls Method E. Method E is deep in your code, while Methods B and A are higher up. If you anticipate that Method E might throw an exception, you should create a try/catch block deep in your code to catch that exception as close as possible to the place where the problem arises. You might also want to create more general exception handlers higher up in the code in case unanticipated exceptions slip by.
In some instances, throwing an exception and unwinding the stack can create a problem. For example, if you have opened a file or otherwise committed a resource, you might need an opportunity to close the file or flush the buffer.
If you must take some action, such as closing a file, regardless of whether an exception is thrown, you have two strategies to choose from. One approach is to enclose the dangerous action in a try block and then to close the file in both the catch and try blocks. However, this is an ugly duplication of code, and it's error prone. VB.NET provides a better alternative in the finally block.
The code in the finally block is guaranteed to be executed regardless of whether an exception is thrown. You begin a finally block with the keyword Finally and end it with the End Try statement.
A finally block can be created with or without catch blocks, but a finally block requires a try block to execute. It is an error to exit a finally block with Exit, Throw, Return, or Goto. The TestFunc( ) method in Example 11-5 simulates opening a file as its first action. The method then undertakes some mathematical operations, and then the file is closed.
It is possible that some time between opening and closing the file an exception will be thrown. If this were to occur, it would be possible for the file to remain open. The developer knows that no matter what happens, at the end of this method the file should be closed, so the file close function call is moved to a finally block, where it will be executed regardless of whether an exception is thrown. Example 11-5 uses a finally block.
Option Strict On Imports System Namespace ExceptionHandling Class Tester Public Sub Run( ) Try Console.WriteLine("Open file here") Dim a As Double = 5 Dim b As Double = 0 Console.WriteLine("{0} / {1} = {2}", a, b, DoDivide(a, b)) Console.WriteLine("This line may or may not print") ' most derived exception type first Catch e As System.DivideByZeroException Console.WriteLine("DivideByZeroException caught!") Catch Console.WriteLine("Unknown exception caught!") Finally Console.WriteLine("Close file here.") End Try End Sub 'Run ' do the division if legal Public Function DoDivide( _ ByVal a As Double, ByVal b As Double) As Double If b = 0 Then Throw New System.DivideByZeroException( ) End If If a = 0 Then Throw New System.ArithmeticException( ) End If Return a / b End Function 'DoDivide Shared Sub Main( ) Console.WriteLine("Enter Main...") Dim t As New Tester( ) t.Run( ) Console.WriteLine("Exit Main...") End Sub 'Main End Class 'Tester End Namespace 'ExceptionHandling Output: Enter Main... Open file here DivideByZeroException caught! Close file here. Exit Main...
In Example 11-5, one of the catch blocks from Example 11-4 has been eliminated to save space, and a finally block has been added. Whether or not an exception is thrown, the finally block is executed; thus in both examples the following message is output:
Close file here.
So far you've been using the exception as a sentinelthat is, the presence of the exception signals the errorsbut you haven't touched or examined the Exception object itself. The System.Exception object provides a number of useful methods and properties.
The Message property provides information about the exception, such as why it was thrown. The Message property is read-only; the code throwing the exception can pass in the message as an argument to the exception constructor, but it cannot be modified by any method once set in the constructor.
The HelpLink property provides a link to the help file associated with the exception. This property is read/write. In Example 11-6, the Exception.HelpLink property is set and retrieved to provide information to the user about the DivideByZeroException. It is generally a good idea to provide a help link file for any exceptions you create, so that the user can learn how to correct the exceptional circumstance.
The read-only StackTrace property is set by the runtime. This property is used to provide a stack trace for the error statement. A stack trace displays the call stack: the series of method calls that lead to the method in which the exception was thrown.
Option Strict On Imports System Namespace ExceptionHandling Class Tester Public Sub Run( ) Try Console.WriteLine("Open file here") Dim a As Double = 5 Dim b As Double = 0 Console.WriteLine("{0} / {1} = {2}", a, b, DoDivide(a, b)) Console.WriteLine("This line may or may not print") ' most derived exception type first Catch e As System.DivideByZeroException Console.WriteLine( _ "DivideByZeroException! Msg: {0}", e.Message) Console.WriteLine( _ "Helplink: {0}", e.HelpLink) Console.WriteLine( _ "Stack trace: {0}", e.StackTrace) Catch Console.WriteLine("Unknown exception caught!") Finally Console.WriteLine("Close file here.") End Try End Sub 'Run ' do the division if legal Public Function DoDivide( _ ByVal a As Double, ByVal b As Double) As Double If b = 0 Then Dim e as new System.DivideByZeroException( ) e.HelpLink = "http://www.LibertyAssociates.com" Throw e End If If a = 0 Then Throw New System.ArithmeticException( ) End If Return a / b End Function 'DoDivide Shared Sub Main( ) Console.WriteLine("Enter Main...") Dim t As New Tester( ) t.Run( ) Console.WriteLine("Exit Main...") End Sub 'Main End Class 'Tester End Namespace 'ExceptionHandling Output: Enter Main... Open file here DivideByZeroException! Msg: Attempted to divide by zero. HelpLink: http://www.libertyassociates.com Stack trace: at ExceptionHandling.Tester.DoDivide(Double a, Double b) in ...Module1.vb:line 38 at ExceptionHandling.Tester.Run( ) in ...Module1.vb:line 10 Close file here. Exit Main...
In the output of Example 11-6, the stack trace lists the methods in the reverse order in which they were called; by reviewing this order, you can infer that the error occurred in DoDivide( ), which was called by Run( ). When methods are deeply nested, the stack trace can help you understand the order of method calls and thus track down the point at which the exception occurred.
In this example, rather than simply throwing a DivideByZeroException, you create a new instance of the exception:
Dim e As New System.DivideByZeroException( ) Throw e
You do not pass in a custom message, and so the default message will be printed:
DivideByZeroException! Msg: Attempted to divide by zero.
If you want, you can modify this line of code to pass in a custom message:
Dim e As New System.DivideByZeroException( _ "You tried to divide by zero which is not meaningful")
In this case, the output message will reflect the custom message:
DivideByZeroException! Msg: You tried to divide by zero which is not meaningful
Before throwing the exception, you set the HelpLink property:
e.HelpLink = "http://www.libertyassociates.com"
When this exception is caught, the program prints both the message and the HelpLink:
Catch e As System.DivideByZeroException Console.WriteLine( _ "DivideByZeroException! Msg: {0}", e.Message) Console.WriteLine( _ "Helplink: {0}", e.HelpLink)
The Message and HelpLink properties allow you to provide useful information to the user. The exception handler also prints the StackTrace by getting the StackTrace property of the exception object:
Console.WriteLine( _ "Stack trace: {0}", e.StackTrace)
The output of this call reflects a full StackTrace leading to the moment the exception was thrown. In this case, only two methods were executed before the exception, DoDivide( ) and Run( ):
Stack trace: at ExceptionHandling.Tester.DoDivide(Double a, Double b) in Module1.vb:line 38 at ExceptionHandling.Tester.Run( ) in Module1.vb:line 10
Note that I've shortened the pathnames; your printout might look a little different.
The intrinsic exception types the CLR provides, coupled with the custom messages shown in the previous example, will often be all you need to provide extensive information to a catch block when an exception is thrown.
There will be times, however, when you want to provide more extensive information to or need special capabilities in your exception. It is a trivial matter to create your own custom exception class; the only restriction is that it must derive (directly or indirectly) from System.ApplicationException. Example 11-7 illustrates the creation of a custom exception.
Option Strict On Imports System Namespace ExceptionHandling ' custom exception class Public Class MyCustomException Inherits System.ApplicationException Public Sub New(ByVal message As String) ' pass the message up to the base class MyBase.New(message) End Sub 'New End Class 'MyCustomException Class Tester Public Sub Run( ) Try Console.WriteLine("Open file here") Dim a As Double = 0 Dim b As Double = 5 Console.WriteLine("{0} / {1} = {2}", a, b, DoDivide(a, b)) Console.WriteLine("This line may or may not print") ' most derived exception type first Catch e As System.DivideByZeroException Console.WriteLine( _ "DivideByZeroException! Msg: {0}", e.Message) Console.WriteLine("HelpLink: {0}", e.HelpLink) ' catch custom exception Catch e As MyCustomException Console.WriteLine( _ "MyCustomException! Msg: {0}", e.Message) Console.WriteLine("HelpLink: {0}", e.HelpLink) Catch ' catch any uncaught exceptions Console.WriteLine("Unknown exception caught") Finally Console.WriteLine("Close file here.") End Try End Sub 'Run ' do the division if legal Public Function DoDivide( _ ByVal a As Double, ByVal b As Double) As Double If b = 0 Then Dim e As New DivideByZeroException( ) e.HelpLink = "http://www.libertyassociates.com" Throw e End If If a = 0 Then ' create a custom exception instance Dim e As New _ MyCustomException("Can't have zero divisor") e.HelpLink = _ "http://www.libertyassociates.com/NoZeroDivisor.htm" Throw e End If Return a / b End Function 'DoDivide Shared Sub Main( ) Console.WriteLine("Enter Main...") Dim t As New Tester( ) t.Run( ) Console.WriteLine("Exit Main...") End Sub 'Main End Class 'Tester End Namespace 'ExceptionHandling Output: Enter Main... Open file here MyCustomException! Msg: Can't have zero divisor HelpLink: http://www.libertyassociates.com/NoZeroDivisor.htm Close file here. Exit Main...
MyCustomException is derived from System.ApplicationException and consists of nothing more than a constructor that takes a string message that it passes to its base class.
|
The advantage of creating this custom exception class is that it better reflects the particular design of the Test class, in which it is not legal to have a zero divisor. Using the ArithmeticException rather than a custom exception would work as well, but it might confuse other programmers because a zero divisor wouldn't normally be considered an arithmetic error.
Top |