This documentation is archived and is not being maintained.

Life Without On Error Goto Statements

Visual Studio .NET 2003
 

Deborah Kurata
InStep Technologies, Inc

July 11, 2003

Summary: In previous versions of Visual Basic, the best practice for handling errors was put On Error Goto in every routine, but there is no On Error statement in Visual Basic .NET. This article describes how to use new Visual Basic .NET features to handle errors without using On Error Goto statements. (8 printed pages)

Contents

Introduction
Catching Exceptions
Throwing Exceptions
Catching Custom Exceptions
Conclusion

Introduction

This is the second in a series of articles that describe the fundamental changes in Visual Basic® and how to do today with Visual Basic .Net what you used to do in prior versions of Visual Basic. The first article covered life without control arrays. This article looks at how to implement error handling in your application without using On Error Goto statements.

There are three types of errors that can occur in your application:

  1. Anticipated errors: These are errors that your application can anticipate, such as attempting to read a file that does not exist or attempting to open a connection with an invalid connection string.
  2. Unanticipated errors: These are errors that occur in your application due to unexpected conditions, such as a programming or data error.
  3. Business rule violations: These could be data entry errors, such as the user entering alpha characters into a numeric field, or they could be more complex business logic issues, such as attempting to delete an order line item for an order that has already been shipped.

The original versions of Visual Basic provided On Error Goto for catching and handling of any errors in your application. For anticipated errors, the On Error Goto could catch the error and then your code could attempt to recover. For unanticipated errors, the On Error Goto could catch the error and then your code could terminate gracefully, without the user seeing a system error message. For business rule violations, your code could raise a specific error number and then the On Error Goto statement could catch the error and display a user-friendly message.

However, On Error Goto had some limits. Its goto style syntax made your routines structurally complex. And if you did not remember to exit the routine before the error handling at the bottom, it was easy to accidentally fall through the code into the error handling. It was difficult to have clean up code that ran in all cases (regardless of whether or not an error occurred).

Visual Basic .NET has a rich set of features that provide all of the features of On Error Goto, without the limitations.

Note   Actually, Visual Basic .NET does support On Error Goto through the Microsoft Visual Basic .NET Compatibility library. This library allows you to retain some of the Visual Basic 6.0 features in Visual Basic .NET to simplify the migration process. Features of this library should be used only for migration.

Catching Exceptions

In .NET terms, errors are no longer called errors, but rather exceptions. Anticipated errors, unanticipated errors, and business rule violations are all considered to be exceptions.

After writing any routine, it is always a good idea to think about the exceptions that the routine could cause (anticipated errors), any unexpected exceptions that the routine could generate (unanticipated errors), and any business rules that the routine could violate.

For example, many applications use a login form or page to control access to the application and its functions. The code to validate the login is executed when the user clicks on the login button as follows:

Private Sub cmdLogin_Click(ByVal sender As Object, _
      ByVal e As System.EventArgs) Handles cmdLogin.Click
   Dim oUser As User()
   Dim bValid as Boolean
   oUser = New User()
   bValid = oUser.ValidateLogin(txtUserName.Text, txtPassword.Text)
   If bValid then
      DialogResult = DialogResult.OK
   End If
   oUser.Dispose
   oUser = Nothing
End Sub

This code creates a new instance of the User class and then calls its ValidateLogin method to access the database and validate the user-entered username and password. If the login is valid, it sets DialogResult to OK to close the login form. It then disposes of the User class instance and returns.

There are several places in this code that an exception could occur. The line of code that creates the new instance from the User class could generate an unanticipated exception if the instance cannot be created for some reason. The ValidateLogin method could generate anticipated exceptions (such as an invalid connection string), unanticipated exceptions (such as a missing table field or stored procedure), or business rule violations (such as passing an empty user name).

Instead of adding an On Error Goto to catch these exceptions, the exceptions can be caught using a .NET Try/Catch block. The Try/Catch syntax makes it easier to catch and process exceptions in a structured manner; hence the reason that .NET exception handling is often referred to as structured exception handling (SEH).

A Try/Catch block could be added to the code as follows:

Private Sub cmdLogin_Click(ByVal sender As Object, _
      ByVal e As System.EventArgs) Handles cmdLogin.Click
   Dim oUser As User()
   Dim bValid as Boolean
 Try
   oUser = New User()
   bValid = oUser.ValidateLogin(txtUserName.Text, txtPassword.Text)
   If bValid then
      DialogResult = DialogResult.OK
   End If
   oUser.Dispose
   oUser = Nothing
 Catch ex As Exception
   MessageBox.Show(ex.Message)
 End Try
End Sub

If any exception occurs in the Try block, the Catch block picks up the exception and the code within the Catch block will execute. In this case, the catch will grab any exception, assign the exception to the ex variable, and display a message box containing the exception message. If no exception occurs, the Catch block code is ignored.

If you look closely at the example above, you will notice that the code to dispose of the instance won't be executed if an exception occurs. To correct this, the code could be repeated in the Catch block, but that means duplicating code.

A better approach would be to use the optional Finally block within the Try/Catch block as follows:

Private Sub cmdLogin_Click(ByVal sender As Object, _
      ByVal e As System.EventArgs) Handles cmdLogin.Click
   Dim oUser As User()
   Dim bValid as Boolean
 Try
   oUser = New User()
   bValid = oUser.ValidateLogin(txtUserName.Text, txtPassword.Text)
   If bValid then
      DialogResult = DialogResult.OK
   End If
 Catch ex As Exception
   MessageBox.Show(ex.Message)
 Finally
   oUser.Dispose()
   oUser = Nothing
 End Try
End Sub

The code in the Finally block will be executed after the Try block completes successfully, after the Catch block executes, or after any Return statements are executed in the Try or Catch blocks. Any code that needs to be executed before leaving the routine should be added to the Finally block.

Notice how the declaration of the User object was done outside of the Try block. This is required if the object variable will be accessible both from the Try block and the Finally block because .NET has block-scoped variables.

In prior version of Visual Basic, there were three types of variable scoping:

  • Global-level variables were accessible to the entire application.
  • Module-level variables were accessible in the code file (form, class, or module) in which they were declared.
  • Local variables were accessible only in the routine in which they were declared.

In .NET, there is a fourth type of scoping—block-level scoping. Variables declared within a block, such as a Try block or For/Next block, are only accessible within the block.

If the declaration of the User object had been inside of the Try block, the Finally block would not be able to reference the variable. So object variables that will need to be disposed in the Finally block must be declared outside of the Try block.

Throwing Exceptions

One of the reasons that exceptions are not called errors is that the term error frequently implies a coding mistake. An exception is any violation of a routine's implicit assumptions. The .NET Framework will throw exceptions to your application if your code violates any of the .NET Framework implicit assumptions. For example, the .NET Framework assumes that a divisor will be a non-zero number. If your code attempts to divide by 0, an exception will be thrown. You can then catch these exceptions using the Try/Catch block.

When writing your routines, you should follow the same guidelines and throw exceptions when any implicit assumption is violated. To throw an exception, use the Throw statement and throw a new instance of the appropriate exception class. (See the online help for the list of .NET exceptions that you can throw.)

For example, the ValidateLogin method makes the assumption that it should receive non-empty values as the parameters. If the values are empty, it should throw an ArgumentOutOfRange exception.

Public Function ValidateLogin(ByVal sUserName As String, _
      ByVal sPassword As String) As Boolean
   If sUserName.length=0 OrElse sPassword.Length=0 Then
      Throw New ArgumentOutOfRangeException("Username and password are required.")
   End If
   ' Code to validate login here
   Return True
End Function

This Throw statement creates a new instance of the ArgumentOutOfRangeException and defines the message text. When this statement is executed, the exception is thrown.

The code following the Throw statement is not executed, but rather the .NET runtime looks for a Try/Catch block. If the current code is not in a Try block, the .NET runtime looks up the call stack to see if the code that called this method is in a Try block. If it finds a Try block, it then looks for an associated Catch block. If the .NET runtime finds an appropriate Try/Catch block, it executes the code in the Catch block. Otherwise, it displays the unhandled exception message and terminates the application.

Note   As the .NET runtime looks for associated Try blocks up the call stack, it will execute any code in the associated Finally block of the Try blocks before continuing up the call stack.

In addition to throwing .NET exceptions, you may find that you want to define your own custom exceptions. In the login example, in addition to throwing the ArgumentOutOfRangeException you may want to throw a custom exception if the username is not valid and a different custom exception if the password is not valid.

To create a custom exception, you can create your own exception class. To ensure that it behaves as a .NET exception, your new exception class should inherit from one of the .NET exception classes. The recommended class to use for your inheritance is the ApplicationException class. For example, the UsernameNotFoundException class would look like this:

Public Class UsernameNotFoundException : Inherits ApplicationException
   Public Sub New()
      MyBase.New()
   End Sub
   Public Sub New(ByVal message As String)
      MyBase.New(message)
   End Sub
   Public Sub New(ByVal message As String, ByVal innerEx As Exception)
      MyBase.New(message, innerEx)
   End Sub
End Class

When using inheritance, the new class automatically has all of the properties and methods of the inherited class. So, the UsernameNotFoundException class has all of the standard ApplicationException properties, such as the message and call stack.

The new class does not inherit any of the constructors of the inherited class; hence the need for this class to have its own constructors. The ApplicationException class supports three constructors:

  • One with no parameters
  • One with just the message parameter
  • One with both a message and an inner exception

The last constructor is used in the case when the code catches an exception and then re-throws it as a different exception, but wants to retain the original exception information.

You can also add custom properties and methods to your new exception class. For example, you could add a username property to your exception class so that you could log any exceptions and include the username of the user that had the exception.

The routine can throw custom exceptions as follows:

Public Function ValidateLogin(ByVal sUserName As String, _
      ByVal sPassword As String) As Boolean
   If sUserName.length=0 OrElse sPassword.Length=0 Then
      Throw New ArgumentOutOfRangeException("Username and password are required.")
   End If
   ' Code to locate the user record here
   If iUserRecordID = 0 Then
      Throw New UsernameNotFoundException("Invalid username")
   End If
   ' Code to retrieve the password from the user record here
   If sPassword <> sUserRecordPassword Then
      Throw New PasswordInvalidException("Invalid password")
   End If
   Return True
End Function

This routine then throws either a .NET exception or a custom exception if any of the routine's implicit assumptions are violated.

Notice that there is no Try/Catch block around this code. You will find that most of your methods won't need Try/Catch blocks. Rather, all of your event procedure code will be your line of defense, catching any exceptions thrown by any of the methods called by those event procedures.

Catching Custom Exceptions

The Try/Catch block code shown at the beginning of this article provided a generic exception handler using a generic exception filter—ex as Exception. This filter would catch any .NET exception or custom exception that inherited from a .NET exception. In the login example, the generic exception filter would correctly catch any exception thrown from the ValidateLogin method.

There may be cases, however, when the code needs to perform different processes depending on which exception was thrown. A Try/Catch block can contain any number of Catch blocks with more explicit exception filters that can catch specific custom or .NET exceptions and perform processing for each type of exception.

Private Sub cmdLogin_Click(ByVal sender As Object, _
      ByVal e As System.EventArgs) Handles cmdLogin.Click
   Dim oUser As User()
   Dim bValid as Boolean
 Try
   oUser = New User()
   bValid = oUser.ValidateLogin(txtUserName.Text, txtPassword.Text)
   If bValid then
      DialogResult = DialogResult.OK
   End If
 Catch ex As UsernameNotFoundException
   MessageBox.Show(ex.Message)
   txtUserName.Focus()
 Catch ex As PasswordInvalidException
   MessageBox.Show(ex.Message)
   txtPassword.Focus()
 Catch ex As Exception
   MessageBox.Show(ex.Message)
 Finally
   oUser.Dispose()
   oUser = Nothing
 End Try
End Sub

The order of these exception filters is important. The more specific filters should always be defined before the generic filters. The most generic filter (ex as Exception) should always be the last filter to ensure that any unanticipated exception is caught.

Conclusion

Exception handling in Visual Basic has changed, but it has only gotten better. You can now build structured exception handlers to catch any type of error or business rule violation. With Try/Catch/Finally and the ability to inherit your own exception classes from the .NET exceptions, we won't be missing On Error Goto!

Deborah Kurata is a software developer and the best-selling author of Doing Objects with Visual Basic 6.0. She is among the highest rated speakers at software development conferences worldwide and is the co-founder of InStep Technologies, a leading software consulting and training firm.

Show: