How to Have a Method Continue Even After a Catch Method
- Download source - 15.75 KB
Introduction
Error handling is an important part of any software developers' job. The .NET Framework offers some very powerful tools to handle errors in a relatively easy manner.
One of these tools is the Try
... Catch
... Finally
... statement. Yet in the few months that I have been programming, I have seen quite some (wrong) uses of the Try
... Catch
... Finally
... not only by beginning progammers, but also by so-called seniors and gurus!
There are many books and articles discussing correct error handling in .NET, but many fail to give a simple yet complete overview of the so-important Try
... Catch
... Finally
... block.
With this article, I hope to inform any software developer about some proper and detailed uses of the Try
... Catch
... Finally
... block.
Using the Example Project
For this article, I have made a small example application dealing with various uses of the Try
... Catch
... Finally
... statement. The example is easy to use and debug.
The example consists of a single form with some buttons on it. Each button demonstrates a specific use of the Try
... Catch
... Finally
... block and should be debugged using breakpoints to fully understand. Note that the code itself does not actually do anything. It simply throws exceptions and handles them.
The project was made using Visual Studio 2010 Express Version using .NET Framework 4.0. However, all of the code could easily be used in any .NET 3.5 application as well.
Some Basics about Exceptions
No matter how well you test an application, eventually an error will always occur. Perhaps a connection to a database fails, or you are trying to open a file which does not exists on the users' computer or to which the user has no access. If such an error occurs, the .NET Framework gathers information about what just went wrong and stores this in an Exception object which is then thrown up (or down?) the stack. The .NET Framework knows numerous types of Exceptions, such as the FileNotFoundException
, IndexOutOfRangeException
and the SqlException
. Some of these Exception
classes provide specific information about what just happened. For example, the SqlException
contains information about the severity of the SqlError
.
No matter what kind of Exception is thrown, they all inherit from the System.Exception
class. When an Exception
is thrown by the .NET Framework, it automatically looks for the first Catch
block (contained in a Try
... Catch
... block) in the stack. If the stack does not contain any Catch
es, an Unhandled Exception occurred. This will give your end-user a prompt saying that an Unhandled Exception occurred and that they can continue and ignore the error or quit the application. What happens next is a mystery. In the best case, your application is closed either way. In the worst case... Let's just not think about that. :)
As a developer, you generally do not want users to see such a message. Therefore, it is important to always implement error handling in your code by using Try
... Catch
... blocks.
Starting the Sample Project
When you open the example project, you will see a form with numerous buttons.
I know the big red button looks tempting, but let's not press it right away. Instead, first build the project. Now go to your debug folder (usually located in your solution folder) and run the executable. Now you may press the big red button. That does not look very good, does it?
Now press any other of the buttons (except Close) and notice the difference. Go back to the project file again. Before running the application, I want you to press the shortcut keys Control + Alt + E in Visual Studio. This will display the Exception Dialog. Make sure both checkboxes behind 'Common Language Runtime Exceptions' are checked. This will make sure that you go into debug mode whenever an Exception is thrown in your code. It is recommended that you fully step through the code of any button by using Breakpoints and F8 to follow the paths that Exceptions make when dealing with Try
... Catch
... Finally
blocks.
A Simple Try... Catch...
Open the project and press the first button on the form, simple Try
... Catch
.... You will immediately be sent to the source code. It is not a very exciting piece of code, but this is where it all starts.
Private Sub btnSimpleTryCatch_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnSimpleTryCatch.Click Try Throw New System.Exception(" An unexpected error occurred") Catch ex As Exception MessageBox.Show(ex.Message, Me.Text, _ MessageBoxButtons.OK, MessageBoxIcon.Error) End Try End Sub
Notice what happens. I am trying (Try...) to run some code, but for some reason, an Exception
is thrown. Instead of crashing my application, the Exception
looks for the first Catch
it can find. Fortunately, a Catch
happens to be nearby and the Exception
can be handled. Be aware of the fact that any code between the Throw
and the Catch
is NOT executed. The Exception
jumps over this code and goes straight to the Catch
block. Also notice that I Catch ex as Exception. Remember that every type of Exception
inherits from the Exception
object? That means that this little bit of code is able to catch ANY Exception
! In this case, I simply let the user know that something went wrong by using a MessageBox
and showing the Message
property of the Exception
object. Now, whatever code is between the Try
... Catch
... and whatever might go wrong is automatically handled by showing a prompt to the user. Easy, no?
A Simple Try... Catch... Finally...
Let's move on to the second button on the form. Click it and notice that your cursor turns into a waiting cursor! Unfortunately after a few seconds, all goes wrong...
Private Sub btnSimpleTryCatchFinally_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnSimpleTryCatchFinally.Click Me.Cursor = Cursors.WaitCursor Try Threading.Thread.Sleep(5000) Throw New System.Exception(" An unexpected error occurred.") Catch ex As Exception MessageBox.Show(ex.Message, Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Error) Finally Me.Cursor = Cursors.Default End Try End Sub
This piece of code looks very much like that of the previous button, with the exception that this is a long process and we turn the cursor into a wait cursor. I have often waited a long time for applications that appeared to be loading, but had actually crashed which caused my cursor to always stay in waiting mode! So how do we make sure the cursor in OUR application always turns back to default? Easy, we use a Finally
... block. The Finally
... statement in a Try
... Catch
... Finally
... block is ALWAYS executed. So if everything would go as it should, my code would be executed, it would skip the Catch
block, go into my Finally
block and put my cursor back to normal. If an Exception
occurs (like here), the code jumps into the Catch
block, handles the error and then goes into my Finally
block, making sure that my cursor is turned back to normal. Notice how I initially change my cursor outside of the Try
... Catch
... Finally
... block. This makes sure that when I go into my Finally
, my cursor is actually a wait cursor. In this example, it would not matter if the first line of code is inside or outside the Try
... Catch
... Finally
... block. But it will matter later on as you will see. I should also mention that the Try
... Finally
... block can also be used without the Catch
. We will see an example of that later on.
Multiple Catches
In some cases, you might want to handle certain Exception
s differently than others. The following example provides an explanation of multiple Catch
es in a single Try
... Catch
... block. For the example, let's just say we are trying to open a file on the users' computer. Anything could happen. Maybe the directory of the file you are trying to open does not exist, maybe the file does not exist, maybe the user has no rights to open the file or maybe something completely unexpected occurs. In this case, you can use multiple catches, as the following code snippet shows:
Private Sub btnMultipleCatches_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnMultipleCatches.Click Try Dim rand As New Random If rand.Next(2) = 1 Then Throw New IO.FileNotFoundException_ (" The file you are trying to open was not found.") Else Throw New System.Exception(" An unknown error occurred.") End If Catch dirEx As IO.DirectoryNotFoundException MessageBox.Show(dirEx.Message, " Directory not found", _ MessageBoxButtons.OK, MessageBoxIcon.Error) Catch fileEx As IO.FileNotFoundException MessageBox.Show(fileEx.Message, " File not found", _ MessageBoxButtons.OK, MessageBoxIcon.Error) Catch ioEx As IO.IOException MessageBox.Show(ioEx.Message, " IO exception", _ MessageBoxButtons.OK, MessageBoxIcon.Error) Catch ex As Exception MessageBox.Show(ex.Message, " Unknown exception", _ MessageBoxButtons.OK, MessageBoxIcon.Error) End Try End Sub
Note how an Exception
is generated using the Random
class. 50% of the time, we will get a FileNotFoundException
and 50% of the time, we will get a non-specific Exception
.
At this point, you might want to uncheck the checkbox you checked at the beginning of this article by using Control + Alt + E and press the 'Multiple catches' button a couple of times to see how it can show two different MessageBox
es (either the "File not found" or the "Unknown exception" MessageBox
es).
If the Exception
is thrown, it will look at the first Catch
block that it can use. In this case, for the FileNotFoundException
this is the Catch fileEx As IO.FileNotFoundException. Notice that the FileNotFoundException
skips the Catch dirEx As IO.DirectoryNotFoundException block, because it is no DirectoryNotFoundException
. Also, if the fileEx has been handled in the fileEx block, it will not step into the ioEx or the ex block anymore.
Both the FileNotFoundException
and the DirectoryNotFoundException
inherit from the IOException
. This means that if I had put the Catch ioEx As IO.IOException block above the other two Catch
blocks, the FileNotFoundException
and the DirectoryNotFoundException
would never jump in their respective Catch
blocks!
When handling Exception
s of multiple types, you must first specify the most specific Exception
block. The general Catch ex as Exception should always be on the bottom of the hierarchy because every Exception
can jump into this block, preventing them from jumping into more specific Catch
blocks after that.
A Finally
... block could still be added at the bottom of the Try
... Catch
... block.
If you have unchecked the 'Common Language Runtime Exception' checkbox by using Control + Alt + E, do not forget to turn it on again.
Exceptions from Deeper Methods
A common misconception about Try
... Catch
... is that every block of code should have one. This is not true in many (if not most) cases. To illustrate this, I have made a class named "ManyMethods
" which exposes three Public
members which step into a couple of methods. None (with the exception of one) of these methods contain a Try
... Catch
... block. Yet, the last method where all of these methods will end eventually always throws an Exception
. If none of the methods has a Try
... Catch
... block, then where will this Exception
be handled? Easy, in the first block of code on the stack that does have a Try
... Catch
... block. In this case, the event handler of the button.
Private Sub btnExceptionsFromDeeperMethods_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnExceptionsFromDeeperMethods.Click Try Dim mm As New ManyMethods mm.GoIntoDeeperMethods() Catch ex As Exception MessageBox.Show(ex.Message & Environment.NewLine & _ " The method which threw me was: " & ex.TargetSite.Name, _ Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Error) End Try End Sub Public Sub GoIntoDeeperMethods() B() MessageBox.Show(" You will never see me!", " Some deeper method", _ MessageBoxButtons.OK, MessageBoxIcon.Information) AA() AB() End Sub Private Sub ExceptionMethod() Throw New System.Exception(" I am coming from way down here!") End Sub
So even though none of the methods that this piece of code jumps into has a Try
... Catch
... block, the Exception
will simply come back here and let the user know something went wrong.
Also note that I use the 'TargetSite
' property of the Exception
object. This property contains the method which threw the Exception
(see the System.Reflection.MethodBase
http://msdn.microsoft.com/en-us/library/system.reflection.methodbase.aspx namespace for more information). I am using this property to prove that the Exception
really was thrown a few methods in the calling method, but that if an Exception
occurs, it can simply be handled in the first Try
... Catch
... block, no matter where in the stack it is.
Also check the other methods in the ManyMethods
class. They are all called by the methods the code runs through. Yet, because an Exception
occurs, the calling code will never execute.
Finally in Deeper Methods
This example is almost the same as the previous one, with one difference. In this example, I put a method between my button Event Handler and the method that eventually calls the ExceptionMethod
. This extra method contains a Try
... Finally
... block. So click the button and see what happens.
Private Sub btnFinallyInDeeperMethods_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnFinallyInDeeperMethods.Click Try Dim mm As New ManyMethods mm.GoIntoDeeperMethodsWithFinally() Catch ex As Exception MessageBox.Show(ex.Message & Environment.NewLine & _ " The method which threw me was: " & ex.TargetSite.Name, _ Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Error) End Try End Sub Public Sub GoIntoDeeperMethodsWithFinally() Try GoIntoDeeperMethods() Finally MessageBox.Show(" You will always see me!", _ " Finally", MessageBoxButtons.OK, MessageBoxIcon.Information) End Try End Sub
As you can see, when you press the button and step through the code, the Finally
... statement in this block of code will be executed, even though the Exception
is not thrown or handled here. Any Finally
... block between the throwing of an Exception
and the handling of an Exception
will always be executed. This is especially useful when you have to Dispose
of objects or make changes to the user interface (such as transforming the cursor in the first Finally
... example).
Handling an Exception Multiple Times
When an Exception
is thrown from a Class
that is called from a Form
, it is possible that you want to do some Exception
Handling in the throwing Class
before you handle the Exception
in your Form
to let users know something went wrong.
A practical example would be a Class
that stores data to your database using a Transaction
object. When an error occurs you want to rollback your transaction before you throw the Exception
up to the calling Form
.
A Finally
... block would not suffice, because you would not know if the Transaction needs to be rolled back or committed. Handling the Exception in the Class itself is necessary, but an Exception can only be handled once and it would not be thrown to your calling Form
.
There are basically two options you can consider. You either rethrow the Exception
after you have handled it in your Class
or you throw a new Exception
, possibly passing the original Exception
as an InnerException
to the new Exception
. The following code snippet shows the last option:
Private Sub btnHandlingAnExceptionMultipleTimes_Click_ (ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles btnHandlingAnExceptionMultipleTimes.Click Try Dim mm As New ManyMethods mm.GoIntoDeeperMethodsWithCatch() Catch ex As Exception MessageBox.Show(ex.Message & Environment.NewLine & _ " The method which threw me was: " & _ ex.TargetSite.Name & Environment.NewLine & _ " The method which initially threw me was: _ " & ex.InnerException.TargetSite.Name, _ Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Error) End Try End Sub Public Sub GoIntoDeeperMethodsWithCatch() Try GoIntoDeeperMethods() Catch ex As Exception MessageBox.Show(" Do some specific error handling here...", _ " Some deeper method", MessageBoxButtons.OK, MessageBoxIcon.Information) Dim newEx As New System.Exception(" I am rethrown!", ex) Throw newEx End Try End Sub
The button Event Handler calls a method, which in turn calls other methods. Note that the second method here also has a Try
... Catch
... block. Eventually, you will get an Exception
which will be sent to the first Catch
block it finds on the stack. In this case, this will be the Catch
in the GoIntoDeeperMethodsWithCatch
. But further down the stack, there is another Catch
block that wants to notify the user that something went wrong.
So the first Catch
block handles the Exception
and then throws a new Exception
with the original Exception
as its InnerException
. The InnerException
property of the Exception
object can contain another Exception
. So what happens here is that the GoIntoDeeperMethodsWithCatch
method handles the original Exception
and then throws a new Exception
to the Form
. The Form
then has information about the new Exception
and the original Exception
(which is contained in the InnerException
property of the new Exception
). Be careful when using the InnerException
property of the Exception
object. If no InnerException
is provided, the above code will cause a NullReferenceException
. An alternative to the above code is to simply Catch ex as Exception in the GoIntoDeeperMethodsWithCatch
method and then Throw ex
. In this case, the original Exception
is thrown to the Form
or Class
above. Because in this case, your Exception
will not have an InnerException
you would have to edit the Exception
handling in the button Event Handler too. Basically you would only create a new Exception
if you want to add some information to the original Exception
. For example, that the first Exception
has already been handled.
Nested Try... Catch... Finally...
Sometimes, you want to do multiple error handlings in one block of code. In this case, it is possible to use nested Try
... Catch
... Finally
... blocks.
Private Sub btnNestedTryCatch_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnNestedTryCatch.Click Try Try Dim table As New DataTable Try Throw New System.Exception(" I come from the third nested try.") Finally table.Dispose() End Try Catch ex As Exception MessageBox.Show(ex.Message & Environment.NewLine & _ " I was handled in the second Try... Catch... block.", _ Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Error) End Try Catch ex As Exception MessageBox.Show(ex.Message & Environment.NewLine & _ " I was handled in the first Try... Catch... block.", _ Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Error) End Try End Sub
In this example, there are three nested Try
... blocks. The third one has no Catch
, but a Finally
only. This is no problem though, because if an Exception
occurs here, it will simply be thrown to the second Try
... block which does have a Catch
that handles the Exception
.
If an Exception
occurs in the second or third nested Try
... block, the Exception
will never reach the first Try
... block (unless it is rethrown). If the Exception
occurs in the first block, it will never reach the second and third Try
... blocks.
The Finally
... will only be executed if your code makes it to the third Try
... block. Of course you can also add Finally
... blocks in the first and second Try
... blocks and you can also add multiple Catch
es (for SqlExc
eption, IOException
, etc.).
Also note that I have two variables named ex
. This means that the ex
variable only exists within the Catch
block. Also, I declare the DataTable
in the second nested Try
... block which means it does not exist in the first Try
... block, making it impossible to Dispose
it in a Finally
... block in the first Try
... statement.
Using... End Using
If you are using an object that implements the IDisposable
interface, you would generally do well if you actually disposed of it after you are done using it. The above examples illustrate that this can be done by using a Try
... Finally
... block. In the Finally
... block, you can call the Dispose
method of the object you are using. There is a shorter notation for these situations though.
Private Sub btnUsingEndUsing_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnUsingEndUsing.Click Dim table1 As New DataTable Try Finally table1.Dispose() End Try Using table2 As New DataTable End Using MessageBox.Show(" This button does not actually do something." _ & Environment.NewLine & " Check the source code :)", _ Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Error) End Sub
The Using
... End Using
block Dispose
s any object for you, no matter what happens. This example only shows the shortened syntax for the Try
... Finally
... Dispose
pattern. Nested Using
... End Using
blocks are also allowed, as we will see in the final example.
To Sum It Up...
This final example combines some of the above examples, using Try
... Catch
... Finally
and Using
blocks. This example does create an actual Exception
, so it will not be artificially thrown from the code. Let's look at this practical example.
Private Sub btnToSumItUp_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnToSumItUp.Click Me.Cursor = Cursors.WaitCursor Try DoSomeSqlStuff() Catch sqlEx As SqlClient.SqlException MessageBox.Show(sqlEx.Message, " SQL Exception", _ MessageBoxButtons.OK, MessageBoxIcon.Error) Catch ex As Exception MessageBox.Show(ex.Message, " Unknown exception", _ MessageBoxButtons.OK, MessageBoxIcon.Error) Finally Me.Cursor = Cursors.Default End Try End Sub Private Sub DoSomeSqlStuff() Using connection As New SqlClient.SqlConnection_ (" Data Source=SomeServer;Initial Catalog=SomeDB;Integrated Security=True") Using cmd As New SqlClient.SqlCommand_ (" SELECT * FROM SomeTable", connection) connection.Open() Try Using reader As SqlClient.SqlDataReader = cmd.ExecuteReader While reader.Read End While End Using Finally connection.Close() End Try End Using End Using End Sub
In this example, my button Event Handler tries to connect to a database. Before it does that, the cursor is transformed into a wait cursor.
In the called method, I declare a Connection
object and pass a valid connection string to it. The Connection
object implements IDisposable
, so I can use the Using
statement to declare it. Note that the connection
variable only exists within the Using
... End Using
block. Anything I want to do with the connection
should therefore be done in the Using
... End Using
block.
Next, I declare a Command
object in the same way I declared the Connection
object. I pass the connection to the Command
objects constructor.
Next, I try to open the connection. This will of course never work with the provided connectionstring
. The syntax is all right, but the Data Source and Initial Catalog (your SQLServer
instance) simply do not exist! So, opening the connection will fail and throw an Exception
.
Note that the code does not reach the Try
... Finally
... block and the connection will not be closed. This is all right, because we could not open it in the first place. Only after the connection is opened, it would be important to close it and thus get into the Try
... Finally
... block. The code will now first get to the End Using
statements and dispose of your Command
object and Connection
object (in that order). The Exception
is then handled one method up, in the button Event Handler. Here, it can be caught as an SqlException
(which is the case for this Exception
) or as any other Exception
. After the Exception
is handled, the code will go into the Finally
... block and change my cursor back to default.
An alternative approach to DoSomeSqlStuff
would be the following code, which does not make use of the Using
... End Using
statement for the Connection
and Command
objects.
Private Sub AlternativeDoSomeSqlStuff() Dim connection As New SqlClient.SqlConnection_ (" Data Source=SomeServer;Initial Catalog=SomeDB;Integrated Security=True") Dim cmd As New SqlClient.SqlCommand(" SELECT * FROM SomeTable", connection) Try connection.Open() Using reader As SqlClient.SqlDataReader = cmd.ExecuteReader While reader.Read End While End Using Finally connection.Close() connection.Dispose() cmd.Dispose() End Try End Sub
Note here that if the application fails to create a new Connection
object (for example because the connection string is not syntaxically correct), the code will never jump into the Try
... Finally
... block, but go straight to the error handling in the button Event Handler. If the creation of the Command
object fails, you would also not go into the Try
... Finally
... block and your Connection
object would not be disposed. The chances of that happening are really very small though, so in this case it would be smart to neglect it, or you would have to add another Try
... Finally
... block, making your code long and difficult to read (although the Using
... End Using
block would be prepared for such a rare Exception
). Also note that if the connection fails to open, it will still be closed in the Finally
... block. Though it is not really necessary, it will not throw an Exception
and it makes your code more compact and readable to not add another Try
... Finally
... block here. If you want, you could check the State
property of your connection object before closing it though.
Points of Interest
- The
Try
...Catch
...Finally
... block should be used to handleException
s ONLY!!! Too often have I seen actual business logic inTry
...Catch
... blocks. This will make your software depend onException
s. And you do not want that! - Always try to handle
Exception
s at user interface level. When something goes wrong, you want to notify the user about this. Do not try to do this in underlying classes. This will make your classes less usable in other objects. Think for example that you have aClass
that handles e-mailing in your application. The first form to implement e-mailing is the company products form. When the e-mailing fails and throws anException
, you do not want your class to prompt the user that the product information could not be e-mailed. Because if you start using thisClass
in other forms too, let's say a sales order form, it would look kind of weird if you could not e-mail product information (you are mailing sales order information!). - You can handle
Exception
s in underlying classes, but do not communicate them to the user from there. If you want to handle anException
in aClass
other than your user form, rethrow theException
up the stack instead (or pass it as anInnerException
to a newException
object). Think where, when and how you want to throw and handleException
s. - Experiment with
Exception
s. You can create your ownException
classes by inheriting from theSystem.Exception
class and adding properties and/or functionality.
History
- 5th February, 2011: Initial post
Source: https://www.codeproject.com/articles/154121/using-try-catch-finally
0 Response to "How to Have a Method Continue Even After a Catch Method"
Post a Comment