Lesson 1 - Exceptions in C# .NET
In this course, we're going to focus on working with files. However, before we can begin to read and write files, we'll have to talk about handling error states, which will occur a lot when working with files.
Errors will often occur in our programs. I'm not referring to errors caused due to poorly written code, we can avoid such errors pretty well. I'm mainly referring to errors caused by input/output operations, often referred to as I/O. The most common culprits for these sort of errors are user input from the console, files in general, output to a file, output to a printer, and so on. In all of these cases, there is a user who can enter invalid input or nonexistent/invalid files, unplug the printer, and so on. However, we won't let them crash our programs due to errors. Instead, we'll sanitize all of the vulnerable places in the program and inform the user about the situation.
Active error handling
The first approach of dealing with program errors is called active error handling. We'll map all of the vulnerable spots in the program and sanitize them with conditions. Dividing by zero is one of the most common and easily resolved errors. Imagine a program that uses a class called Mathematics, which has a Divide() method. The class might look something like this:
class Mathematics { public static int Divide(int a, int b) { return a / b; } ... }
If we use the class this way:
Console.WriteLine("Enter the dividend and the divisor to calculate the result:"); int a = int.Parse(Console.ReadLine()); int b = int.Parse(Console.ReadLine()); Console.WriteLine(Mathematics.Divide(a, b));
If the user enters the numbers 12 and 0, the program crashes with an error because we can't divide by zero. However, we can sanitize the error actively with a simple condition in the program:
Console.WriteLine("Enter the dividend and the divisor to calculate the result:"); int a = int.Parse(Console.ReadLine()); int b = int.Parse(Console.ReadLine()); if (b != 0) Console.WriteLine(Mathematics.Divide(a, b)); else Console.WriteLine("You cannot divide by zero.");
Now, every time we use this method we have to make sure we don't pass zero to the second parameter. Imagine if the method took 10 parameters and was used at several points in the program. It'd most likely be a nuisance to sanitize all of this method's different uses.
One solution would be to add sanitation directly into the method. However, we would then have a new problem: What value will it return when the second parameter is zero? We need a value that would tell us when the calculation wasn't successful. This is a major issue because if we choose to return zero, we wouldn't know whether the calculation causes an error or not. We have no way of knowing whether 0 is a result or an error. Even negative numbers won't help in this case. The .NET framework features the TryParse() method which solves a similar issue by returning a bool type and passing another "return" value as a modified parameter. However, this could be very unclear. Parsing values is the second classic example of user input vulnerability. The next example is file operations where the file may not exist, we don't have to have the required privileges to access the file, or the file is being used in another program.
Passive error handling
We use exceptions when the operation is complex and it'd be too difficult to sanitize all of the possible error states. Exceptions are referred to as a passive error handling. We don't have to deal with the internal logic of the method we call in any way. All we have to do is try to run the vulnerable part of the code in a "protected mode". This mode is slightly slower and different in that if an error occurs, we are able to catch it and prevent the application from crashing. We refer to an error as to an exception here. We use try-catch blocks to catch exceptions:
try { } catch { }
We put the vulnerable part of the code into the try block. If an error occurs in the try block, its execution is interrupted and the program jumps to the catch block. If everything goes as expected, the entire try block is executed and the catch block is skipped. Let's test them out on our previous example:
try { Console.WriteLine(Mathematics.Divide(a, b)); } catch { Console.WriteLine("Error occurred."); }
The code is simpler since we don't need to sanitize all of the vulnerable spots and think about everything that could go wrong. We simply wrap the dangerous code using the try block, and all of the errors will be caught in the catch block. Of course, we only put what is absolutely necessary in the try block, not the entire program
Now we know how to handle situations where the user enters some input that could cause an error. Exceptions have a very wide range of use which is by no means limited to file operations. For example, the Int.TryParse() method, which we use to treat inputs, could be written using a try-catch block. We could alter our program so that it cannot be broken by the user.
Using exceptions while working with files
As mentioned before, file operations can cause many exceptions, so we always work with files in try-catch blocks. There are several other constructs that can be used with exceptions.
Finally
We are able to add a third block to the try-catch block called the finally block. This block is always executed whether an exception has occurred or not. Consider the following method for saving application settings (we'll keep our file handling methods simple for now):
public void SaveSettings() { try { OpenFile("file.dat"); WriteToFile(settings); } catch { Console.WriteLine("An error has occurred."); } if (FileIsOpened()) CloseFile(); }
The method tries to open a file and add a settings object to it. If an error occurs, it displays a message to the console. An opened file has to be closed in all cases. However, printing errors directly in a method isn't a great approach. We are all aware that methods and objects, in general, should only perform the logic, and the communication with the user should be performed in the layer which calls them. Now, let's assign a return value of the bool type to the method and return true/false depending on whether the operation succeeded or not:
public bool SaveSettings() { try { OpenFile(); WriteToFile(); return true; } catch { return false; } if (FileIsOpened()) CloseFile(); }
At first sight, it seems like the file will always be closed. However, the
code is in a method in which we call return
. As you already know,
the return
statement terminates the method and nothing after it
will be performed.
Therefore, the file would always remain open. As a result, the file could
become inaccessible. If we put the file closing part into the
finally
block, it would always be executed. C# remembers
that the try-catch block contained finally
and calls the finally
block even after leaving the try or catch block:
public bool SaveSettings() { try { OpenFile(); WriteToFile(); return true; } catch { return false; } finally { if (FileIsOpened()) CloseFile(); } }
Therefore, finally
is used for resource
cleaning routines along with exceptions, i.e. closing files, freeing
memory, and so on.
The entire situation was greatly simplified. For each file type, there are C# writer and reader classes. A method for storing settings in C# .NET would look something more like this:
public bool SaveSettings() { SomeFileWriter w = null; try { w = new SomeFileWriter("file.dat"); w.Write(obj); return true; } catch { return false; } finally { if (w != null) w.Close(); } }
First, we assign null to the writer instance. Then, we try to create the
writer over the file.dat file, in the try block and write an object. If
everything is successful, we return true (we also add a finally block at the
very end). The operation could still fail for two reasons. We could be unable to
write to the file or are unable to open the file for writing at all. In both
cases, we catch the exception and return false based on what we assume is the
reason that the method was unable to save the data. Last of all, we close the
file opened by the writer in the finally
block. Since opening the
file could be unsuccessful, we'll have to ask whether the writer was created at
all before we proceed to close it. We could call the method like this:
if (!SaveSettings()) Console.WriteLine("Unable to save settings.");
It would be even better to omit the catch
block and let the
method throw an exception.We'll suppose it'll be the method which called our
method who will catch it, not our method. It's better like that because we avoid
having to use a return value here (which could then be used for something else),
and the code is simpler:
public void SaveSettings() { SomeFileWriter w = null; try { w = new SomeFileWriter("file.data"); w.Write(obj); } finally { if (w != null) w.Close(); } }
We could call the method like this:
try { SaveSettings(); } catch { Console.WriteLine("Unable to save settings."); }
Now, let's simplify the entire situation even more via the
using
construct.
Using
C# allows us to greatly simplify working with instances of classes used for
reading and writing to files. The block above can be rewritten using the
using
notation which replaces the
try
and finally
blocks. The biggest advantage
to using
is that the finally
block is generated
automatically and it ensures that the instance of the reader or writer closes
the file. The SaveSettings() method with the using
construct would look like this:
public void SaveSettings() { using (SomeFileWriter w = new SomeFileWriter("file.dat")) { w.Write(obj); } }
As you can see, the code has been extremely simplified (it still does the
same thing). However, when we call the method, we have to use the try-catch
block again. Remember that using
only replaces the
try-finally blocks, not the catch block!. The method, in which
using
is in, must be called in a try-catch block.
Now, we're exactly where we need to be. We'll use using
for all
file manipulations in further lessons. The code will be simpler and files will
be closed automatically for us.
We'll return to exceptions once again, and learn how to exclusively catch exceptions of a specific type. You'll also learn which exception classes we're able to use in our programs and how to create your own exceptions. For now, however, the bare minimum of information for working with files will do. I didn't want to confuse you with complex constructs this early in the course
In the next lesson, Introduction to working with files, we'll learn about writing privileges for files in Windows and try out some simple file operations.