Get up to 80 % extra points for free! More info:

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/in­valid 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.


 

All articles in this section
Files and I/O in C# .NET
Skip article
(not recommended)
Introduction to working with files
Article has been written for you by David Capka Hartinger
Avatar
User rating:
2 votes
The author is a programmer, who likes web technologies and being the lead/chief article writer at ICT.social. He shares his knowledge with the community and is always looking to improve. He believes that anyone can do what they set their mind to.
Unicorn university David learned IT at the Unicorn University - a prestigious college providing education on IT and economics.
Activities