The structured exception-handling system used by .NET is a highly efficient and powerful way to detect and handle errors. But if you've never dealt with exceptions before—like most Visual Basic programmers looking at .NET—it can be a little intimidating, possibly making you run back to your On Error Goto roots. In this article, I'll introduce .NET's structured exception-handling system and explain why it's superior to using return values and error codes.
What is an exception?
In simple terms, an exception represents an abnormal situation in an application—an "exceptional condition," if you will. Usually, this will be an error of some kind, and the exception itself will describe the error condition in some way. When an exception occurs in a program, it is passed up through the call chain until one of two things happens: Either a handler is found that is capable of handling the exception or the exception is tossed out of the application's main method, in which case, the default exception handler is triggered. This usually unceremoniously ends the program
Not surprisingly, in .NET, exceptions are represented as objects that ultimately inherit from the System.Exception object. Exceptions are said to be "thrown" when an error occurs and "caught" when an error is handled. Handling an exception requires a code construct known as a Try…Catch block, which, in its simplest form, looks like the code in Figure A.
Your basic Try…Catch block in VB.NET and C#
Any exception occurring in code protected a Try block, even in a called function or method that doesn't itself contain a Try block, will be handled by the code inside the Catch block. Unless, of course, the Catch block itself throws an exception, in which case it will be thrown to the next higher Try block, even if that means throwing the exception out of the currently executing function.
,b>The plot exceptionally thickens
Sounds pretty familiar so far: We've got an error-handling mechanism that will jump to an indicated error-handling block if an error occurs in the code protected by the handler. If no handler appears in the currently executing code, or if an error occurs in the handler, the error is propagated up the call stack. Here's where you run into the first disconnect between what you are used to and what you've got now—and it's called the Finally block.
The Finally block in a Try statement contains code that will always be executed whenever an exception occurs. Including one is optional, unless your Try does not have a matching Catch—a Try must always end with one or the other. You'd place any cleanup code you wanted to always execute, regardless of whether an error actually occurs, inside a Finally block. This might include code to close files, database connections, or other limited resources. Note that Finally code executes immediately after Catch block code, unless an exception is thrown from the Catch block.
Here are a few other things to keep in mind:
Simultaneously handling multiple exceptions
When handling errors in VB6, you usually had to resort to an If or Case block that examined Err.Number if you wanted to figure out exactly what error occurred. This often resulted in complex error-handling logic that was itself prone to error and sometimes wound up being significantly longer and more complex than the code it was protecting.
In .NET, you can eliminate the If Err.Number=5647828312 syndrome by taking advantage of the fact that all exceptions are objects and therefore have an intrinsic type that can be compared to another type. A variation on the Catch statement can take advantage of this ability to handle only a particular exception type, as shown in Figure B.
Catching a particular exception type
You can set a Catch statement to be as specific or general as you like. For example, the first Catch statement in Figure B will catch only exceptions of type System.NullReferenceException, the second will catch only exceptions descending from System.ArgumentException, while the third will catch any exceptions not explicitly caught by the other two. In this last case, the exception is explicitly thrown out of the current Try block and would be handled in the next appropriate Catch block found by the CLR as it crawled up the call stack. As it turns out, this final Catch block is redundant, as that's the exact behavior you would see if it were not included.
This short example illustrates four important rules about exceptions:
This is pretty cool stuff, allowing your code to selectively deal with only the
errors that it has the ability to deal with, trusting that others will be passed
up the call stack for notification purposes, at least. In practice, because of
the overhead involved in having the CLR watch for thrown exceptions, using a
single Try block with multiple exception-specific Catch
statements, as we saw in Figure B, is the best way to check for multiple
specific errors in a block of code.
Rolling your own exceptions
You can also create and throw exceptions in your code yourself to signal the occurrence of system-defined errors without having them actually occur. All of the exception classes provide overloaded constructors, allowing you to include specific information about the cause of the exception. It's usually a good idea to do so and to include at least an explanatory message when creating the exception.
The astute among you will have deduced that if one can explicitly create and throw a system-defined exception, one must also be able to create and throw one's own exceptions. These custom exceptions usually represent application-defined errors, and should always extend the System.ApplicationException class.
Replacing the often-ignored return code
At one time or another, we've all written code that relies on functions returning meaningful error codes to inform the caller of the nature of an error, should one occur. The main problem with this strategy is that it's possible for the caller to neglect to check the return code and erroneously assume that a call succeeded. Using exceptions makes it impossible to ignore the fact that an error occurred in a function call. If the caller fails to catch an exception thrown by the function it called, it is completely bypassed, and execution moves up the call chain to the last exception handler that was encountered.
A second problem is that, unless you are careful to ensure otherwise, there can be little rhyme or reason to what return codes equate to which error conditions. Developers sometimes deal with this problem by assigning return code ranges or prefixes to particular families of error messages. Exceptions, by virtue of their status as full-fledged objects, have meaning in their specific types. They can have an inheritance hierarchy as simple or as complex as required to enable you to handle them as either specific error conditions or as related groups of errors.
Breaking the Goto habit
Depending on your point of view, the fact that On Error error handling is alive and well in VB.NET is either fortunate or unfortunate. It works exactly as it did with your VB6 code and has been thoroughly updated to be compatible with .NET's structured exception system. Yeah, that's right, you can still use the good old Err object and not miss a beat because it exposes a GetException method that returns a System.Exception object representing whatever the current error might be. The Err.Raise method throws true exceptions that can be caught by Try blocks (even those written in other .NET languages) and On Error statements alike. It's even possible to catch exceptions by their equivalent error number in VB.NET, further blurring the line between exception handling and error handling.
I tend to fall into the group that considers the persistence of On Error in VB.NET unfortunate. I actually find the fact that VB.NET's error-handling system is so schizophrenic to be a bit of an embarrassment. The truth is that this is a horrible mishmash of error-handling functionality that has been kept around simply for backward compatibility, and I, for one, hope it won't be supported for long. It's also unique to VB.NET and therefore won't be found in any other .NET languages. So sticking with the VB6-style error handling means you are tied to VB.NET and lose one of the big advantages of .NET development—the freedom to use the best language for the job.
You see, sometimes, change can be good.