Every application might encounter errors during its execution, even when you spend several nights on testing the application and all the possible execution scenarios. Runtime errors are especially unpredictable because the application execution is conditioned by user actions. Because of this, error handling is a fundamental practice that, as a developer, you need to know in depth. In this chapter you learn how the .NET Framework enables handling errors and how to get information to solve problems deriving such errors. In other words, you learn about .NET exceptions.
In development environments other than .NET, programming languages can handle errors occurring during the application execution in different ways. For example, the Windows native APIs return a 32-bit HRESULT
number in case an error occurs. Visual Basic 6 uses the On Error
statements, whereas other languages have their own error-handling infrastructures. As you can imagine, such differences cannot be allowed in the .NET Framework because all languages rely on the Common Language Runtime (CLR), so all of them must intercept and handle errors the same way. With that said, the .NET Framework identifies errors as exceptions. An exception is an instance of the System.Exception
class (or of a class derived from it) and provides deep information on the error that occurred. Such an approach provides a unified way for intercepting and handling errors. Exceptions occur at runtime as well as during the application execution. Exceptions can be thrown by the Visual Basic compiler at compile time or by the background compiler when typing code. Errors occurring when designing the user interface of an application or when working within the Visual Studio 2012 IDE are typically called exceptions. This is because such tasks (and most of the Visual Studio IDE) are powered by the .NET Framework. In Chapter 2, “Getting Started with the Visual Studio 2012 IDE,” we introduced the Visual Studio debugger and saw how it can be used for analyzing error messages provided by exceptions (see the “About Runtime Errors” section in Chapter 2). In that case, we did not implement any error-handling routine because we were just introducing the debugging features of Visual Studio during the development process. But what if an error occurs at runtime when the application has been deployed to your customer without implementing appropriate error-handling code? Imagine an application that attempts to read a file that does not exist and in which the developer did not implement error checks. Figure 6.1 shows an example of what could happen and what should never happen in a real application.
As you can see from Figure 6.1, in case of an error the application stops its execution and no resume is possible. Moreover, identifying the type of error occurred can also be difficult. Because of this, as a developer it is your responsibility to implement code for intercepting exceptions and take the best actions possible to solve the problem—while keeping your users’ choices in mind. The best way to understand exceptions is to begin to write some code that causes an error, so this is what we do in the next section.
Visual Basic 2012 enables deep control over exceptions. With regard to this, an important concept is that you not only can check for occurring exceptions, but can also conditionally manage solutions to exceptions and raise exceptions when needed. In this section we discuss all these topics, providing information on how you can intercept and manage exceptions in your application.
One of the (very few) commonalities between Visual Basic 6 and Visual Basic .NET and higher is the syntax approach. This should help a little more in migrating from Visual Basic 6 to 2012. Although Visual Basic 2012 (more precisely, VB.NET from 2002 to 2012) still enables the usage of the On Error Goto
and On Error Resume
statements, when developing .NET applications with Visual Basic, you should never use such statements for two reasons. First, exceptions are the only thing that enables interoperation with other .NET languages such as Visual C# and Visual F#. This is fundamental when developing class libraries or components that could potentially be reused from other languages than Visual Basic. The second reason is that the old-fashioned way for handling errors is not as efficient as handling .NET exceptions. If you decided to migrate, you should completely forget On Error
and exclusively use exceptions.
System.Exception
is the most general exception and can represent all kinds of errors occurring in applications. It is also the base class for derived exceptions, which are specific to situations you encounter. For example, the System.IOException
derives from System.Exception
, is thrown when the application encounters input/output errors when accessing the disk, and can be handled only for this particular situation. On the other hand, System.Exception
can handle not only this situation, but also any other occurring errors. You can think of System.Exception
as the root in the exceptions hierarchy. We explain later in code the hierarchy of exception handling. Classes representing exceptions always terminate with the word Exception
. You encounter exceptions such as FileNotFoundException
, IndexOutOfRangeException
, FormatException
, and so on. This is not mandatory, but a recommended naming convention. Generally, .NET built-in exceptions inherit from System.Exception
, but because they are reference types, you find several exceptions inheriting from a derived exception.
You perform exception handling by writing a Try..Catch..Finally
code block. The logic is that you say to the compiler, “Try to execute the code; if you encounter an exception, take the specified actions; whenever the code execution succeeds or it fails due to an exception, execute the final code.” The most basic code for controlling the execution flow regarding exceptions is the following:
Try
'Code to be executed
Catch ex As Exception
'Code to handle the exception
End Try
IntelliSense does a great job here. When you type the Try
keyword and then press Enter, it automatically adds the Catch
statement and the End Try
terminator. The ex
variable gets the instance of the System.Exception
that is caught, and that provides important information so that you can best handle the exception. To see what happens, consider the following code snippet:
Try
Dim myArray() As String = {"1", "2", "3", "4"}
Console.WriteLine(myArray(4))
Catch ex As Exception
Console.WriteLine(ex.Message)
End Try
Here we have an array of strings in which the upper range of the array is 3. The Try
block tries to execute code that attempts writing the content of the fourth index to the Console window. Unfortunately, such an index does not exist, but because the code is formally legal, it will be correctly compiled. When the application runs and the runtime encounters this situation, it throws an exception to communicate the error occurrence. So the Catch
statement intercepts the exception and enables deciding which actions must be taken. In our example, the action to handle the exception is to write the complete error message of the exception. If the code within Try
succeeds, the execution passes to the first code after the End Try
terminator. In our example, the control transfers to the Catch
block that contains code that writes to the Console window the actual error message that looks like the following:
Index was outside the bounds of the array
The runtime never throws a generic System.Exception
exception. There are specific exceptions for the most common scenarios (and it is worth mentioning that you can create custom exceptions as discussed in Chapter 12, “Inheritance”) that are helpful to identify what happened instead of inspecting a generic exception. Continuing our example, the runtime throws an IndexOutOfRangeException
that means the code attempted to access and index greater or smaller than allowed. Based on these considerations, the code could be rewritten as follows:
Try
Dim myArray() As String = {"1", "2", "3", "4"}
Console.WriteLine(myArray(4))
Catch ex As IndexOutOfRangeException
Console.WriteLine("There is a problem: probably you are "
& Environment.NewLine &
" attempting to access an index that does not exist")
Catch ex As Exception
Console.WriteLine(ex.Message)
End Try
As you can see, the most specific exception needs to be caught before the most generic one. This is quite obvious because, if you first catch the System.Exception
, all other exceptions will be ignored. Intercepting specific exceptions can also be useful because you can both communicate the user detailed information and decide which actions must be taken to solve the problem. Anyway, always adding a Catch
block for a generic System.Exception
is a best practice. This enables you to provide a general error-handling code in case exceptions you do not specifically intercept will occur. You could also need to perform some actions independently from the result of your code execution. The Finally
statement enables executing some code either if the Try
succeeds or if it fails, passing control to Catch
. For example, you might want to clean up resources used by the array:
Dim myArray() As String = {"1", "2", "3", "4"}
Try
Console.WriteLine(myArray(4))
Catch ex As IndexOutOfRangeException
Console.WriteLine("There is a problem: probably you are "
& Environment.NewLine &
" attempting to access an index that does not exists")
Catch ex As Exception
Console.WriteLine(ex.Message)
Finally
myArray = Nothing
End Try
Notice how objects referred within the Finally
block must be declared outside the Try..End Try
block because of visibility. The code within Finally
will be executed no matter what the result of the Try
block will be. This is important; for example, think about files. You might open a file and then try to perform some actions on the file that for any reason can fail. In this situation you would need to close the file, and Finally
ensures you can do that if the file access is successful and even if it fails (throwing an exception). An example of this scenario is represented in Listing 6.1.
Imports System.IO
Module Module1
Sub Main()
Console.WriteLine("Specify a file name:")
Dim fileName As String = Console.ReadLine
Dim myFile As FileStream = Nothing
Try
myFile = New FileStream(fileName, FileMode.Open)
'Seek a specific position in the file.
'Just for example
myFile.Seek(5, SeekOrigin.Begin)
Catch ex As FileNotFoundException
Console.WriteLine("File not found.")
Catch ex As Exception
Console.WriteLine("An unidentified error occurred.")
Finally
If myFile IsNot Nothing Then
myFile.Close()
End If
End Try
Console.ReadLine()
End Sub
End Module
The code in Listing 6.1 is quite simple. First, it asks the user to specify a filename to be accessed. Accessing files is accomplished with a FileStream
object. Notice that the myFile
object is declared outside the Try..End Try
block so that it can be visible within Finally
. Moreover, its value is set to Nothing
so that it has a default value, although it’s null. If we did not assign this default value, myFile
would just be declared but not yet assigned, so the Visual Basic compiler would throw a warning message. By the way, setting the default value to Nothing
will not prevent a NullReferenceException
at runtime unless the variable gets a value. The Try
block attempts accessing the specified file. The FileStream.Seek
method here is just used as an example needed to perform an operation on the file. When accessing files, there could be different problems, resulting in various kinds of exceptions. In our example, if the specified file does not exist, a FileNotFoundException
is thrown by the runtime and the Catch
block takes control over the execution. Within the block, we just communicate that the specified file was not found. If, instead, the file exists, the code performs a search. In both cases, the Finally
block ensures that the file gets closed, independently on what happened before. This is fundamental because if you leave a file open, other problems would occur.
In Listing 6.1, we saw how we can catch a specific exception, such as FileNotFoundException
, and then the general System.Exception
. By the way, FileNotFoundException
does not directly derive from System.Exception
; instead, it derives from System.IO.IOException
, which is related to general input/output problems. Although you are not obliged to also catch an IOException
when working with files, adding it could be a good practice because you can separate error handling for disk input/output errors from other errors. In such situations, the rule is that you have to catch exceptions from the most specific to the most general. Continuing the previous example, Listing 6.2 shows how you can implement exceptions hierarchy.
Imports System.IO
Module Module1
Sub Main()
Console.WriteLine("Specify a file name:")
Dim fileName As String = Console.ReadLine
Dim myFile As FileStream = Nothing
Try
myFile = New FileStream(fileName, FileMode.Open)
'Seek a specific position in the file.
'Just for example
myFile.Seek(5, SeekOrigin.Begin)
Catch ex As FileNotFoundException
Console.WriteLine("File not found.")
Catch ex As IOException
Console.WriteLine("A general input/output error occurred")
Catch ex As Exception
Console.WriteLine("An unidentified error occurred.")
Finally
If myFile IsNot Nothing Then
myFile.Close()
End If
End Try
Console.ReadLine()
End Sub
End Module
FileNotFoundException
is the most specific exception, so it must be caught first. It derives from IOException
, which is intercepted second. System.Exception
is instead the base class for all exceptions and therefore must be caught last.
The System.Exception
class exposes some properties that are useful for investigating exceptions and then understanding what the real problem is. Particularly when you catch specialized exceptions, it could happen that such exception is just the last ring of a chain and that the problem causing the exception itself derives from other problems. System.Exception
’s properties enable a better navigation of exceptions. Table 6.1 lists the available properties.
Listing 6.3 shows how you can retrieve deep information on the exception. In the example, information is retrieved for the System.IO.FileNotFoundException
, but you can use these properties for any exception you like.
Imports System.IO
Module Module1
Sub Main()
Console.WriteLine("Specify a file name:")
Dim fileName As String = Console.ReadLine
Dim myFile As FileStream = Nothing
Try
myFile = New FileStream(fileName, FileMode.Open)
'Seek a specific position in the file.
'Just for example
myFile.Seek(5, SeekOrigin.Begin)
Catch ex As FileNotFoundException
Console.WriteLine()
Console.WriteLine("Error message: " & ex.Message & Environment.NewLine)
Console.WriteLine("Object causing the exception: "
& ex.Source & Environment.NewLine)
Console.WriteLine("Method where the exception is thrown; "
& ex.TargetSite.ToString & Environment.NewLine)
Console.WriteLine("Call stack:" & ex.StackTrace & Environment.NewLine)
Console.WriteLine("Other useful info:")
For Each k As KeyValuePair(Of String, String) In ex.Data
Console.WriteLine(k.Key & " " & k.Value)
Next
Catch ex As IOException
Console.WriteLine("A general input/output error occurred")
Catch ex As Exception
Console.WriteLine(ex.Message)
Finally
If myFile IsNot Nothing Then
myFile.Close()
End If
End Try
Console.ReadLine()
End Sub
End Module
If you run this code and specify a filename that does not exist, you can retrieve a lot of useful information. Figure 6.2 shows the result of the code.
As you can see from Figure 6.2, the Message
property contains the full error message. In production environments, this can be a useful way to provide customers a user-friendly error message. The Source
property shows the application or object causing the exception. In our example, it retrieves Mscorlib, meaning that the exception was thrown by the CLR. The Target
property retrieves the method in which the exception was thrown. It is worth mentioning that this property retrieves the native method that caused the error, meaning that the method was invoked by the CLR. This is clearer if we take a look at the content of the Stack
property. We can see the hierarchy of method calls in descending order: the .ctor
method is the constructor of the FileStream
class, invoked within the Main
method; the next method, named Init
, attempts to initialize a FileStream
and is invoked behind the scenes by the CLR. Init
then invokes the native WinIOError
function because accessing the file was unsuccessful. Analyzing such properties can be useful to understand what happened. Because there is no other useful information, iterating the Data
property produced no result. By the way, if you just need to report a detailed message about the exception, you can collect most of the properties’ content by invoking the ToString
method of the Exception
class. For example, you could replace the entire Catch ex as FileNotFoundException
block as follows:
Catch ex As FileNotFoundException
Console.WriteLine(ex.ToString)
This edit produces the result shown in Figure 6.3. As you can see, the result is a little different from the previous one. We can find a lot of useful information, such as the name of the exception, complete error message, filename, Call Stack hierarchy, and line of code that caused the exception.
Typically, you use ToString
just to show information, whereas you use properties to analyze exception information.
Catching exceptions is necessary but is also performance consuming. Sometimes you could simply check the value of an object instead of catching exceptions. For example, you could check with an If..Then
statement whether an object is null instead of catching a NullReference exception (when possible, of course). Exceptions are best left to handling “exceptional” situations that occur in code, so you should limit the use of them when possible.
You can also ignore exceptions by simply not writing anything inside a Catch
block. For instance, the following code catches a FileNotFoundException
and prevents an undesired stop in the application execution but takes no action:
Try
testFile = New FileStream(FileName, FileMode.Open)
Catch ex As FileNotFoundException
End Try
You also have the ability to nest Try..Catch..Finally
code blocks. Nested blocks are useful when you have to try the execution of code onto the result of another Try..Catch
block. Consider the following code that shows the creation time of all files in a given directory:
Try
Dim allFiles As String() =
Directory.GetFiles("C:TestDirectory")
Try
For Each f As String In allFiles
Console.WriteLine(File.GetCreationTime(f).ToString())
Next
Catch ex As IOException
Catch ex As Exception
End Try
Catch ex As DirectoryNotFoundException
Catch ex As Exception
End Try
The first Try..Catch
attempts reading the list of files from a specified directory. Because you can encounter directory errors, a DirectoryNotFoundException
is caught. The result (being an array of String
) is then iterated within a nested Try..Catch
block. This is because the code is now working on files and then specific errors might be encountered.
You can exit from within a Try..Catch..Finally
block at any moment using an Exit Try
statement. If a Finally
block exists, Exit Try
pulls the execution into Finally
that otherwise resumes the execution at the first line of code after End Try
. The following code snippet shows an example:
Try
'Your code goes here
Exit Try
'The following line will not be considered
Console.WriteLine("End of Try block")
Catch ex As Exception
End Try
'Resume the execution here
In some situations you need to programmatically throw exceptions or you catch exceptions but do not want to handle them in the Catch
block that intercepted exceptions. Programmatically throwing exceptions can be accomplished via the Throw
keyword. For instance, the following line of code throws an ArgumentNullException
that usually occurs when a method receives a null argument:
Throw New ArgumentNullException
You often need to manually throw exceptions when they should be handled by another portion of code, also in another application. A typical example is when you develop class libraries. A class library must be as abstract as possible, so you cannot decide which actions to take when an exception is caught; this is the responsibility of the developer who creates the application that references your class library. If both developers are the same person, this remains a best practice because you should always separate the logic of class implementations from the client logic (such as the user interface). To provide a clearer example, a class library cannot show a graphical message box or a text message into the Console window. It instead needs to send the code the exception caught to the caller, and this is accomplished via the Throw keyword. We can provide a code example. Visual Studio 2012 creates a new blank solution and then adds a new Class Library project that you could name, for example, TestThrow. Listing 6.4 shows the content of the class.
Imports System.IO
Public Class TestThrow
Public Sub TestAccessFile(ByVal FileName As String)
If String.IsNullOrEmpty(FileName) Then
Throw New ArgumentNullException("FileName",
"You passed an invalid file name")
End If
Dim testFile As FileStream = Nothing
Try
testFile = New FileStream(FileName, FileMode.Open)
Catch ex As FileNotFoundException
Throw New FileNotFoundException("The supplied file name was not found")
Catch ex As Exception
Throw
Finally
If testFile IsNot Nothing Then
testFile.Close()
End If
End Try
End Function
End Class
The TestThrow
class’s purpose is just attempting to access a file. This is accomplished by invoking the TestAccess
method that receives a FileName
argument of type String
. The first check is on the argument: If it is null, the code throws back to the caller an ArgumentNullException
that provides the argument name and a description. In this scenario, the method catches but does not handle the exception. This is thrown back to the caller code, which is responsible to handle the exception (for example, asking the user to specify a valid filename). The second check is on the file access. The Try..Catch..Finally
block implements code that tries to access the specified file and, if the file is not found, it throws back the FileNotFoundException
describing what happened. In this way the caller code is responsible for handling the exception; for example, asking the user to specify another filename. Also notice how a generic System.Exception
is caught and thrown back to the caller by simply invoking the Throw
statement without arguments. This enables the method to throw back to the caller the complete exception information.
Rethrowing Exceptions
Throwing back an exception to the caller code is also known as the rethrow technique.
Now we can create an application that can reference the TestThrow
class library and handle exceptions on the client side. Add to the solution a new Visual Basic project for the Console and add a reference to the TestThrow
class library by selecting Project, Add Reference. Adding a reference to another assembly enables you to use types exposed publicly from such an assembly. Finally, write the code shown in Listing 6.5.
Imports System.IO
Module Module1
Sub Main()
Console.WriteLine("Specify the file name:")
Dim name As String = Console.ReadLine
Dim throwTest As New TestThrow.TestThrow
Try
throwTest.TestAccessFile(name)
Catch ex As ArgumentNullException
Console.WriteLine(ex.ToString & Environment.NewLine &
"You passed an invalid argument")
Catch ex As FileNotFoundException
Console.WriteLine(ex.Message)
Catch ex As Exception
Console.WriteLine(ex.ToString)
Finally
Console.ReadLine()
End Try
End Sub
End Module
The code in Listing 6.5 first asks the user to specify a filename to access. If you press Enter without specifying any filename, the TestAccessFile
method is invoked passing an empty string, so the method throws an ArgumentNullException
, as shown in Figure 6.4.
If this situation happened inside a Windows application, you could provide a MessageBox
showing the error message. With this approach, you maintain logics separately. If you instead specify a filename that does not exist, the caller code needs to handle the FileNotFoundException
; the result is shown in Figure 6.5.
The caller code writes the content of the ex.Message
property to the Console window, which is populated with the error message provided by the Thrown
statement from the class library. If any other kinds of exceptions occur, a generic System.Exception
is handled and its content is shown to the user.
Catching Task-Specific Exceptions
Depending on which tasks you perform within a Try..Catch
block, always catch exceptions specific for those tasks. For example, if your code works with files, don’t limit it to catch a FileNotFoundException
. Consider file-related and disk-related exceptions, too. To get a listing of the exceptions that a class was designed to throw, you can read the MSDN documentation related to that class. The documentation describes in detail which exceptions are related to the desired object.
Invoking the Throw
keyword causes the runtime to search through all the code hierarchy until it finds the caller code that can handle the exception. Continuing our previous example, the Console application is not necessarily the first caller code. There could be another class library that could rethrow the exception to another class that could then rethrow the exception to the main caller code. Obviously, going through the callers’ hierarchy is a task that could cause performance overhead, so you should take care about how many callers are in your code to reduce the overhead. In the end, if the caller code is missing a Catch
block, the result will be the same as what is shown in Figure 6.1, which should always be avoided.
Sometimes you might need to catch an exception only when a particular condition exists. You can conditionally control the exception handling using the When
keyword, which enables taking specific actions when a particular condition is evaluated as True
. Continuing the example of the previous paragraph, the TestThrow
class throws an ArgumentNullException
in two different situations (although similar). The first one is if the string passed to the TestAccessFile
method is empty; the second one is if the string passed to the method is a null value (Nothing
). So it could be useful to decide which actions to take depending on what actually caused the exception. According to this, we could rewrite the code shown in Listing 6.5 as what is shown in Listing 6.6.
Imports System.IO
Module Module1
Sub Main()
Console.WriteLine("Specify the file name:")
Dim name As String = Console.ReadLine
Dim throwTest As New TestThrow.TestThrow
Try
throwTest.TestAccessFile(name)
Catch ex As ArgumentNullException When name Is Nothing
Console.WriteLine("You provided a null parameter")
Catch ex As ArgumentNullException When name Is String.Empty
Console.WriteLine("You provided an empty string")
Catch ex As FileNotFoundException
Console.WriteLine(ex.Message)
Catch ex As Exception
Console.WriteLine(ex.ToString)
Finally
Console.ReadLine()
End Try
End Sub
End Module
As you can see from Listing 6.6, by using the When
keyword you can conditionally handle exceptions depending if the expression on the right is evaluated as True. In this case, when handling the ArgumentNullException
, the condition is evaluating the name variable. If name is equal to Nothing
, the exception is handled showing a message saying that the string is null. If name is instead an empty string (which is different from a null string), another kind of message is shown. When
can be applied only to Catch
statements and works only with expressions. Of course, you can use the When
keyword also with value types; for example, you might have a counter that you increment during the execution of your code and, in case of exceptions, you might decide which actions to take based on the value of your counter. The following code demonstrates:
Dim oneValue As Integer = 0
Try
'perform some operations
'on oneValue
Catch ex As Exception When oneValue = 1
Catch ex As Exception When oneValue = 2
Catch ex As Exception When oneValue = 3
End Try
You do not always need to specify a variable in the Catch
block. This can be the case in which you want to take the same action independently from the exception that occurred. For example, consider the following code:
Try
Dim result As String =
My.Computer.FileSystem.ReadAllText("C:MyFile.txt")
Catch ex As Exception
Console.WriteLine("A general error occurred")
End Try
The ex
variable is not being used and no specific exceptions are handled. So the preceding code can be rewritten as follows, without the ex
variable:
Try
Dim result As String =
My.Computer.FileSystem.ReadAllText("C:MyFile.txt")
Catch
Console.WriteLine("A general error occurred")
End Try
Whichever exception occurs, the code shows the specified message. This also works with regard to the rethrow technique. The following code simply rethrows the proper exception to the caller:
Try
Dim result As String =
My.Computer.FileSystem.ReadAllText("C:MyFile.txt")
Catch
Throw
End Try
Managing errors is something that every developer needs to take into consideration. The .NET Framework provides a unified system for managing errors, which is the exception handling. System.Exception
is the root class in the exceptions hierarchy, and exceptions are handled using a Try..Catch..Finally
block. You can create nested Try..Catch..Finally
blocks and check exceptions conditionally using the When
keyword. In the end, you can programmatically generate exceptions using the Throw
keyword that is particularly useful in class libraries development. This chapter provided a high-level overview of exceptions; now you can decide which kinds of exceptions you need to handle for your code and how to take actions to solve errors.