Saturday, November 11, 2006

Getting a Grip on Structured Exception Handling: Part 2

In Part 1 of this series, we took a look at how errors are handled in Visual Basic using On Error. We saw that it has some architectural problems, it doesn't promote developer discipline, it tends to result in code that's difficult to read and performs less than optimally, and that despite all of this it works.

I can't stress enough that if your code base is working, you should leave it alone. Learning a new technology will likely get you all fired up to use it. Don't whet your appetite on a functioning and stable code base. If you're that excited, create a test project and learn it there. In the name of all that's holy, don't break perfectly good systems just to test your knowledge.

In Part 2 of this series, we'll learn about structured exception handling (SEH): what it is, how it works, and why it's good for you. There are compelling reasons that object-oriented languages use it instead of passing around error numbers and returning error codes from functions.

What is Structured Exception Handling?


Structured exception handling is, in essence, a fancy term for "error handling." An exception is an error. Specifically, it's any condition that prevents your code from doing its work. For example, you might attempt to open a file and discover that it doesn't exist, or that you don't permissions to do so. These conditions would result in exceptions, since they would prevent your application from working.

Some conditions, however, are recoverable. In an attempt to delete a file, you may discover that the file doesn't exist. Here, the file's absence is fine; the user was going to delete it anyway, so you can ignore the exception, and move along.

So far, everything sounds familiar, and it should. It's what you've been doing all along. Only the names have been changed to protect the innocent.

However, structured exception handling differs from using On Error in that it's object-oriented. It uses objects to encapsulate error information. It establishes a clean, predictable behavior for your application when an exception occurs, and allows you to nest error handlers within the same method (something that was rather difficult to do with On Error). It also provides a mechanism for guaranteeing that certain code executes before your method loses control when an exception occurs, allowing you to clean up your resources, resulting in fewer orphaned database connections, file streams, and system resources.

Exceptions aren't raised, they are thrown, and exception handlers catch them. Hence, your exception handler tries to do something, and if your code throws an exception at you, you catch the exception and attempt to handle it. That's the big scary picture of SEH.

The keywords used in SEH are Try, Catch, Finally, End Try, and Throw. In a nutshell, you wrap your code in a Try...End Try block. You catch any exceptions in one or more intervening Catch blocks, and you place your cleanup code in a Finally block. For example:

Protected Overridable Function ReadFile(ByVal sourceFile As String) As String

Dim reader As StreamReader
Dim buffer As String

Try
reader
= New StreamReader(sourceFile)
buffer
= reader.ReadToEnd()
Catch ex As FileNotFoundException
Debug.WriteLine(
"File not found:" & sourceFile)
Throw
Catch ex As PermissionDeniedException
Debug.WriteLine(
"Permission denied:" & sourceFile)
Throw
Catch ex As Exception
Debug.WriteLine(ex.ToString())
Throw
Finally
Disposer.DisposeOf(reader)
End Try

Return buffer

End Function


When an exception occurs within the Try block, the run time scans the list of Catch blocks from top to bottom, searching for a Catch block that accepts an exception that most closely matches the type of the currently thrown exception. If it finds one, the runtime transfers control of the application to that block. If one can't be found, control is passed to the Finally block, if one exists. Your Finally block gets the opportunity to clean up resources prior to the routine losing control.

If the runtime found a Catch block that handled the exception, the Catch block can do any number of things with it. It may quietly consume it, and then continue processing. Or it, may rethrow the exception. In that event, control of the application is passed to the Finally block, as if no handling Catch block were found. The Catch block may throw a new exception. In that event, the Finally block is called, and then control is passed back up the call stack until a suitable exception handler is found.

In the event that no suitable exception handlers are found, a message is either displayed or recorded in the Windows event log (depending on the nature of the application), and the application terminates.


Try Blocks

Place any code that could throw an exception inside the Try block (that's the portion between the Try keyword and the first Catch statement). The .NET Framework documents the exceptions that it will throw, so you should have a very good idea of what exceptions you should be ready to catch, and what statements need to be placed inside the Try block.

Catch Blocks

Exceptions are handled in the Catch block. The catch block always takes exactly one parameter, and it must be an exception. You may define multiple Catch blocks; when doing so, always put the more specific exception types at the top, and the least specific type (System.Exception) at the bottom. (The reason for doing this may not be readily apparent: if you put System.Exception any where else, anything below it will be ignored, because all exceptions are derived from System.Exception. The runtime evaluates exception types from top to bottom; once it finds a match, it will stop looking any further. So remember to put the generic exception at the bottom, as a catch-all. Better yet, if you don't know what to do with it, omit it altogether, and let the caller handle it.)

In general, you do not want to place any statements inside the Catch block if those statements can throw exceptions themselves. If they can, wrap those statements in exception handlers and handle them accordingly.

You are not required to provide any Catch blocks at all. In that event, you must provide a Finally block. This situation is desirable when you want the code in the Finally block to execute even if an exception occurs, but you don't want to handle any of the exceptions that you'll encounter in your method.

Finally Blocks

The last block you may include in a Try...End Try block is the Finally block. You may only include one Finally block. Its contents are executed after your Catch block code has executed, and before control is transferred out of the exception handler. It's your last chance to do anything before your method loses control.

Typically, the code in the Finally block rolls back transactions, closes open files, and cleans up disposable resources. As with a Catch block, it shouldn't invoke methods that can throw exceptions unless it wraps those statements in exception handlers and handles the exceptions.

Throw

To throw an exception, you use the Throw keyword, like this:


Throw New ArgumentNullException("value")


Throw always takes an exception object as its parameter. That's it. As we'll see in a later article in this series, you can create your own custom exceptions, and you can attach additional properties to them (because they're objects) to convey as much information as you need in order to describe the problem.

Structured Exception Handling Example


In the code sample below, we are doing meaningful work and committing a transaction in the Try block, rolling back a transaction in the Catch block, and disposing of resources in the Finally block.

Private Sub InsertEmployee( _
ByVal name As String, _
ByVal employeeID As String, _
ByVal connection As SqlConnection)

If name Is Nothing Then
Throw New ArgumentNullException("name")
ElseIf employeeID Is Nothing Then
Throw New ArgumentNullException("employeeID")
ElseIf name = String.Empty Then
Throw New ArgumentException("name cannot be empty")
ElseIf employeeID = String.Empty Then
Throw New ArgumentException("employeeID cannot be empty")
ElseIf connection Is Nothing Then
Throw New ArgumentNullException("connection")
ElseIf (connection.State And ConnectionState.Open) <> ConnectionState.Open Then
Throw New ArgumentException("Connection is closed.)
End If

Const SqlTemplate As String = _
"INSERT INTO Employee (Name, EmployeeID) VALUES ('{0}', '{1}')"

Dim sql As String = String.Format(SqlTemplate, name, employeeID)
Dim command As SqlCommand
Dim transaction As SqlTransaction

Try
transaction
= connection.BeginTransaction()
command = connection.CreateCommand(sql)
command.CommandType = CommandType.Text
command.Transaction = transaction
command.ExecuteNonQuery()
transaction.Commit()
Catch ex As SqlException()
transaction.RollBack()
Throw
Finally
command.Dispose()
transaction.Dispose()
End Try

End Sub

Moving On


In this article, we've seen the basics of how to handle thrown exceptions, and how to throw them ourselves. Moving forward, we'll cover nesting exception handlers, creating our own exceptions, and we'll dive into when it's appropriate to throw them, and when it's not.

No comments: