I know that up to now my examples have been seriously lacking in error checking. That's because I don't write buggy code.
Seriously, I have avoided major error checking because it sometimes gets in the way of what I really want to show you. However, error checking is very important to any professional-level program.
There is something called exception handling in .NET, where you can wrap some code inside a Try-Catch-Finally block. Any errors that happen because of this code—no matter how serious—will cause the code to end up in the Catch portion of the Try-Catch-Finally block. This is good because it is here that you can detect what happened and do something nice about it.
Most of the examples I have seen concerning throwing and catching errors have been really simplistic. They go something like this.
C#
try { StreamReader file = new StreamReader("MyFile.txt"); string s = file.ReadLine(); int x = int.Parse(s); s = file.ReadLine(); int y = int.Parse(s); int z = x/y; MessageBox.Show("The value of X/Y is " + x.ToString()); } catch { MessageBox.Show("Some error happened"); this.Close(); }
VB
Try Dim file As StreamReader = New StreamReader("MyFile.txt") Dim s As String = file.ReadLine() Dim x As Int32 = Int32.Parse(s) s = file.ReadLine() Dim y As Int32 = Int32.Parse(s) Dim z As Int32 = CInt(x / y) MessageBox.Show("The value of X/Y is " + x.ToString()) Catch MessageBox.Show("Some error happened") Me.Close() End Try
This is catching errors at its simplest. No error trapping at all would have given you a better understanding of what happened.
Essentially, three things could have gone wrong here:
The file might not exist. This would make the StreamReader instantiation fail.
The file might exist but be empty. This would make the read return a null string, which causes an error in the parse line.
The value of "y" might be zero, which throws an error during the division.
How would you know what happened and how would you present the error to the user? Well, you could catch an error. Change the Catch block to read as follows.
C#
catch(Exception ex) { MessageBox.Show(ex.Message); this.Close(); }
Catch ex As Exception MessageBox.Show(ex.Message) Me.Close() End Try
You are now catching the general exception. This is better, but it can still cause some confusion. For instance, if the error caught was in the parsing of the string to an integer, you would get the error shown in Figure 7-13.
Do you know which error this is? I don't. It could have come from either of the two parsing operations.
This next code shows a much more detailed Catch block. It includes specific error catching and detailed error messages.
C#
catch(IOException ex) { MessageBox.Show("File I/O Exception: " + ex.Message); } catch(ArgumentNullException ex) { MessageBox.Show("Parse Error: Null string\n" + ex.Message); } catch(FormatException ex) { MessageBox.Show("Parse Error: string was not numeric\n" + ex.Message); } catch(DivideByZeroException ex) { MessageBox.Show("Math error: 'y' was zero\n" + ex.Message); } catch(Exception ex) { MessageBox.Show(ex.Message); }
VB
Catch ex As IOException MessageBox.Show("File I/O Exception: " + ex.Message) Catch ex As ArgumentNullException MessageBox.Show("Parse Error: Null string\n" + ex.Message) Catch ex As FormatException MessageBox.Show("Parse Error: string was not numeric\n" + ex.Message) Catch ex As DivideByZeroException MessageBox.Show("Math error: 'y' was zero\n" + ex.Message) Catch ex As Exception MessageBox.Show(ex.Message) End Try
As you can see, I catch almost every error that can happen within the Try block. The only thing I can't tell you is which parse line threw the ArgumentNullException or FormatException. This is the preferred method of catching errors. First try to catch the most specific errors and then catch the most general one. This is kind of like a switch-case block in C# (Select-Case in VB), where the last case statement is the default. You have something to fall back on if none of the others is run.
So, is this better than the simple Exception catch? Maybe a little, as it gives more detailed messages, but this code is awfully long and busy. Can you see doing this for every method? Also, what happens in the case of an error (besides showing a message) if there is a failure?
The better way to catch errors like this is before they happen. I can easily adjust this example, where I am opening up a file, reading the contents, parsing some values, and doing a division, so it is more readable and also more robust. Here is what this example should look like.
try { int x, y, z; StreamReader file = new StreamReader("MyFile.txt"); string s = file.ReadLine(); if(s == string.Empty || s == null) throw new ApplicationException("First line was empty"); x = int.Parse(s); s = file.ReadLine(); if(s == string.Empty || s == null) throw new ApplicationException("Second line was empty"); y = int.Parse(s); if(y != 0) { z = x/y; MessageBox.Show("The value of X/Y is " + z.ToString()); } else MessageBox.Show("Unable to divide numbers"); } catch(ApplicationException ex) { MessageBox.Show(ex.Message); //Put a trace message here } catch(IOException ex) { MessageBox.Show("File I/O Exception: " + ex.Message); //Put a trace message here } catch(Exception ex) { MessageBox.Show(ex.Message); //Put a trace message here }
Try Dim file As StreamReader = New StreamReader("MyFile.txt") Dim s As String = file.ReadLine() If s = String.Empty Or s Is Nothing Then Throw New ApplicationException("First line was empty") End If Dim x As Int32 = Int32.Parse(s) s = file.ReadLine() If s = String.Empty Or s Is Nothing Then Throw New ApplicationException("second line was empty") End If Dim y As Int32 = Int32.Parse(s) If y <> 0 Then Dim z As Int32 = CInt(x / y) MessageBox.Show("The value of X/Y is " + x.ToString()) Else MessageBox.Show("Unable to divide numbers") End If Catch ex As IOException MessageBox.Show("File I/O Exception: " + ex.Message) 'Put a trace message here Catch ex As ApplicationException MessageBox.Show(ex.Message) 'Put a trace message here Catch ex As Exception MessageBox.Show(ex.Message) 'Put a trace message here End Try
This code has fewer catches, and it can also catch which line of parse code was invalid. I put some simple error checking in the Try block before I did any major operations. The only thing I can't check for is some unknown I/O error when opening the file. I may not have permission, the file may not exist, and so on. For this I still need to catch the IOException.
By the way, there should be no excuse for a divide by zero exception to appear. Anytime you think that you may have a divisor of zero, check before you do the math. Many programs have blown up due to a divide by zero error that was not caught.
Note |
Notice that I threw an application error with my own message. This allowed me to drill down deeper and get the specific parsing error. You can also create your own errors. See the online help. |
There is more to the Try-Catch block. There is a Finally block that you can also add. The Finally block is interesting in that is it guaranteed to run no matter what. You can take advantage of this in several ways, as I demonstrate in this section.
The Finally block comes last after the Try-Catch block. Its main use is to take care of some housekeeping that may have been skipped over during the Try block. As you have probably figured out, the Try-Catch block jumps over any subsequent code in the Try block if there is an error. There is no way to go back to this code.
Before I go on, I must tell you about one gotcha related to Try blocks. Any object that you want to access outside of the Try block must be defined outside of the Try block. Here is what I mean.
C#
private void foo() { try { StreamReader file = new StreamReader("MyFile.txt"); file.Close(); } catch(Exception ex) { MessageBox.Show(ex.Message); } file.Close(); }
Private Sub foo() Try Dim file As StreamReader = New StreamReader("MyFile.txt") file.Close() Catch ex As Exception MessageBox.Show(ex.Message) End Try File.Close() End Sub
If the StreamReader initialization fails, the first attempt at closing the file will not get run. If you try to compile this code, however, you will get an error stating that the compiler does not recognize the variable "file". This variable was defined inside the Try block and that is the extent of its scope. To have the compiler recognize this "file" variable, I need to define it before the Try block as follows.
C#
private void foo() { StreamReader file; try { file = new StreamReader("MyFile.txt"); file.Close(); } catch(Exception ex) { MessageBox.Show(ex.Message); } if(file != null) file.Close(); }
VB
Private Sub foo() Dim file As StreamReader Try File = New StreamReader("MyFile.txt") file.Close() Catch ex As Exception MessageBox.Show(ex.Message) End Try If Not file Is Nothing Then file.Close() End If End Sub
Now the compiler will be happy. By the way, I made sure that the "file" object was null before I closed it outside of the Try block. I did not do this while inside the Try block. Anyone know why?[2]
Now when you wrap the last few lines in a Finally block, your program should also work. Here is the code.
C#
private void foo() { StreamReader file; try { file = new StreamReader("MyFile.txt"); file.Close(); } catch(Exception ex) { MessageBox.Show(ex.Message); } finally { if(file != null) file.Close(); } }
Private Sub foo() Dim file As StreamReader Try File = New StreamReader("MyFile.txt") file.Close() Catch ex As Exception MessageBox.Show(ex.Message) Finally If Not file Is Nothing Then file.Close() End If End Try End Sub
You may be wondering what the point of the Finally block is. In this case, the point is to get rid of the first instance of closing the file. You see the Finally block is guaranteed to run no matter what. So whether the Try block was successful or not, the file object would still get released before the method ended. This next bit of code shows the true power of this concept.
C#
private void FooBar() { StreamReader file = null; SolidBrush B = new SolidBrush(Color.Azure); Pen P = new Pen(B, 3); Font F = new Font("Arial", 12); Graphics G = null; try { G = Graphics.FromHwnd(this.Handle); file = new StreamReader("MyFile.txt"); string s = file.ReadLine(); G.DrawString(s, F, B, 10, 20); // Do some funky stuff with the pen here // Also do some extensive code //... //... if(s == "The End") return; s = file.ReadLine(); // Do some more stuff with the pen here // Also do some more extensive code //... //... G.DrawString(s, F, B, 30, 20); } catch(Exception ex) { MessageBox.Show(ex.Message); } finally { if(G != null) G.Dispose(); if(F != null) F.Dispose(); if(P != null) P.Dispose(); if(B != null) B.Dispose(); if(file != null) file.Close(); } }
VB
Private Sub FooBar() Dim file As StreamReader = Nothing Dim B As SolidBrush = New SolidBrush(Color.Azure) Dim P As Pen = New Pen(B, 3) Dim F As Font = New Font("Arial", 12) Dim G As Graphics = Nothing Try G = Graphics.FromHwnd(Me.Handle) file = New StreamReader("MyFile.txt") Dim s As String = file.ReadLine() G.DrawString(s, F, B, 10, 20) ' Do some funky stuff with the pen here ' Also do some extensive code '... '... If s = "The End" Then Return End If s = file.ReadLine() ' Do some more stuff with the pen here ' Also do some more extensive code '... '... G.DrawString(s, F, B, 30, 20) Catch ex As Exception MessageBox.Show(ex.Message) Finally If Not G Is Nothing Then G.Dispose() End If If Not F Is Nothing Then F.Dispose() End If If Not P Is Nothing Then P.Dispose() End If If Not B Is Nothing Then B.Dispose() End If If Not file Is Nothing Then file.Close() End If end try End Sub
In this method I instantiate quite a few objects that take up memory. Because it is always nice to clean up after myself, I dispose of these objects in the Finally block.
Now, what is interesting is that there are some return statements in the Try block that bail out of the code depending on some value. This kind of thing happens all the time in software.
I use the Finally block here to make sure that these objects are disposed of before I leave this method. Even though I told the compiler I wanted to return halfway through the code, the Finally block still gets run.
Quite often I will use a Try-Finally block just for this purpose in some complicated or long methods. It is easy to instantiate objects, work with them for a week, and then forget to dispose of them at the end of your code. If you create the Finally block and dispose of every object you create at the same time you type in the code to create them, you will not make that mistake.
Note |
The code for all this Try-Catch stuff is included in the code for this book, which you can obtain from the Downloads section of the Apress Web site (http://www.apress.com). The project is called "PlayCatch." |
[2]Because inside the Try block I know that this object is not null. There's no need to check.