Monday, February 10, 2014

Error and Exception Handling in C#

Critical errors are called Exceptions and they are raised whenever the compiler encounters a problem with a segment of code. Example of common exceptions are divide by zero and reading a null value.
Exceptions can be managed using a try...catch...finally block of code. These will catch any errors and allow your code to handle the error and deal with them without the user's knowledge. Exception handling prevents errors from crashing applications causing data loss.
Traditionally in C, methods and functions would return the error code in the function result. A value of false or -1 usually indicated an error with the method. This caused a few problems, most notably that of returning a value back from a functions, and programmers usually did not test the result code. Also -1 or false could be the result of the function, not necessarily an error, so it was a little confusing.
In the .Net platform we have exceptions, which are System objects that represent a specific or generalised error condition.

Catching an Exception

  1. usingSystem;
  2. usingSystem.Collections.Generic;
  3. usingSystem.Text;
  4.  
  5. namespace ConsoleApplication1
  6. {
  7.     class Program
  8.     {
  9.         staticvoid Main(string[] args)
  10.         {
  11.             int x =0;
  12.             int y =0;
  13.             y =100/ x;
  14.         }
  15.     }
  16. }
Obviously this line of code will cause a exception as you cannot divide any number by 0. If left like this the program will crash and stop working, requiring the user to reload the application. The solution to this is to implement a try...catch...finally block.
Try, catch and finally are three essential code blocks for problem free programs. The logic of the block is that we try to do x, catch an error if one occurs, and finally do y.
If an exception is raised in the try block then the code in the catch block is run. The catch block could call logging or reporting methods, corrective actions or alternative code. Code in the finally block is always executed regardless of an error or not. This block should close files or connections, free memory etc...
Using the divide by zero error above, with a try...catch block implemented it now looks like this.
  1. usingSystem;
  2. usingSystem.Collections.Generic;
  3. usingSystem.Text;
  4.  
  5. namespace ConsoleApplication1
  6. {
  7.     class Program
  8.     {
  9.         staticvoid Main(string[] args)
  10.         {
  11.             int x =0;
  12.             int y =0;
  13.  
  14.             try
  15.             {
  16.                 y =100/ x;
  17.             }
  18.             catch
  19.             {
  20.                 Console.WriteLine("There was an error but we caught it!");
  21.                 Console.WriteLine("Please enter a new number:");
  22.                 y =int.Parse(Console.ReadLine());
  23.             }
  24.         }
  25.     }
  26. }
Should an error occur within the try block, in this example y = 100 / x, the code within the catch block will be executed, in which you should attempt to fix the problem, notify or log the error and gracefully exit the program if required.
Let's have a look at a different example, and see what code gets executed and what code is skipped over. We will also see the finally block in action. In the first example there is no error handling.
  1. usingSystem;
  2. usingSystem.Collections.Generic;
  3. usingSystem.Text;
  4. usingSystem.IO;
  5.  
  6. namespace ConsoleApplication1
  7. {
  8.     class Program
  9.     {
  10.         staticvoid Main(string[] args)
  11.         {
  12.             string filename ="c:\sharpertutorials\orders.csv";
  13.             StreamReader myFile =new StreamReader(filename);
  14.             string orders = myFile.ReadToEnd();
  15.             myFile.Close();
  16.  
  17.             Console.WriteLine(orders);
  18.         }
  19.     }
  20. }
When executed, the file path does not exist, so when we try and open the file an exception is raised.
Error and Exception Handling
As soon as the error occurs, the program cannot continue to run, the user is presented with a horrible error message and the program is forced to close. Not a very good impression.
A better solution is to use a try...catch block to handle the error.
  1. usingSystem;
  2. usingSystem.Collections.Generic;
  3. usingSystem.Text;
  4. usingSystem.IO;
  5.  
  6. namespace ConsoleApplication1
  7. {
  8.     class Program
  9.     {
  10.         staticvoid Main(string[] args)
  11.         {
  12.             string filename ="c:\sharpertutorials\orders.csv";
  13.             string orders =string.Empty;
  14.             StreamReader myFile =null;
  15.            
  16.             try
  17.             {
  18.                 myFile =new StreamReader(filename);
  19.                 orders = myFile.ReadToEnd();
  20.                 myFile.Close();
  21.             }
  22.             catch
  23.             {
  24.                 Console.WriteLine("Sorry, an error has occurred.");
  25.             }
  26.  
  27.             Console.WriteLine(orders);
  28.         }
  29.     }
  30. }
Notice how the variable declarations have been taken outside the try block. This is because we need the variables to be in the scope of the method not just the code block.
When the code is now run, the screen shows a friendly, but rather unhelpful, error message. Luckily there is a solution to this problem! The catch block can take in a parameter which will hold details about the error, so in our example a DirectoryNotFoundException was raised.
  1. namespace ConsoleApplication1
  2. {
  3.     class Program
  4.     {
  5.         staticvoid Main(string[] args)
  6.         {
  7.             string filename ="c:\sharpertutorials\orders.csv";
  8.             string orders =string.Empty;
  9.             StreamReader myFile =null;
  10.            
  11.             try
  12.             {
  13.                 myFile =new StreamReader(filename);
  14.                 orders = myFile.ReadToEnd();
  15.                 myFile.Close();
  16.             }
  17.             catch(DirectoryNotFoundException ex)
  18.             {
  19.                 Console.WriteLine("Sorry, the path to '"+ filename +"' does not exist. Please correct the error and try again.");
  20.             }
  21.  
  22.             Console.WriteLine(orders);
  23.         }
  24.     }
  25. }
Now when the program is run the user have a detailed message telling them what the error is and how to fix it. Let's fix the error by creating the directory and re-run the program.
Error and Exception Handling
Another unhandled exception! This time the orders.csv file does not exist and we have a FileNotFoundException. We can implement multiple catch blocks, one for each type of exception we want to capture.
  1. usingSystem;
  2. usingSystem.Collections.Generic;
  3. usingSystem.Text;
  4. usingSystem.IO;
  5.  
  6. namespace ConsoleApplication1
  7. {
  8.     class Program
  9.     {
  10.         staticvoid Main(string[] args)
  11.         {
  12.             string filename ="c:\sharpertutorials\orders.csv";
  13.             string orders =string.Empty;
  14.             StreamReader myFile =null;
  15.  
  16.             try
  17.             {
  18.                 myFile =new StreamReader(filename);
  19.                 orders = myFile.ReadToEnd();
  20.                 myFile.Close();
  21.             }
  22.             catch(DirectoryNotFoundException ex)
  23.             {
  24.                 Console.WriteLine("Sorry, the path to '"+ filename +"' does not exist. Please correct the error and try again.");
  25.             }
  26.             catch(FileNotFoundException ex)
  27.             {
  28.                 Console.WriteLine("Sorry, the file '"+ filename +"' does not exist. Please create the file and try again.");
  29.             }
  30.  
  31.             Console.WriteLine(orders);
  32.         }
  33.     }
  34. }
This time the user will get another message telling them the cause of the problem and the solution. Now we have a valid file and path, let's try and do something with the data read in from the file.
  1. usingSystem;
  2. usingSystem.Collections.Generic;
  3. usingSystem.Text;
  4. usingSystem.IO;
  5.  
  6. namespace ConsoleApplication1
  7. {
  8.     class Program
  9.     {
  10.         staticvoid Main(string[] args)
  11.         {
  12.             string filename ="c:\sharpertutorials\orders.csv";
  13.             string orders =string.Empty;
  14.             string data =string.Empty;
  15.             StreamReader myFile =null;
  16.  
  17.             try
  18.             {
  19.                 myFile =new StreamReader(filename);
  20.                 orders = myFile.ReadToEnd();
  21.  
  22.                 data = orders.Substring(01);
  23.  
  24.                 myFile.Close();
  25.             }
  26.             catch(DirectoryNotFoundException ex)
  27.             {
  28.                 Console.WriteLine("Sorry, the path to '"+ filename +"' does not exist. Please correct the error and try again.");
  29.             }
  30.             catch(FileNotFoundException ex)
  31.             {
  32.                 Console.WriteLine("Sorry, the file '"+ filename +"' does not exist. Please create the file and try again.");
  33.             }
  34.  
  35.             Console.WriteLine(data);
  36.         }
  37.     }
  38. }
Again, we have another error! Because the file was empty and we tried to do a substring on an empty string we get a ArgumentOutOfRangeException and the program will force close. It gets worse though, since we have opened a file and the program has closed before we closed it! This can lead to all kinds of trouble, even more so if it was a database we were connecting to. The solution is the finally block. Code in the finally block is always guaranteed to run so we can use that to close any files or connections.
  1. usingSystem;
  2. usingSystem.Collections.Generic;
  3. usingSystem.Text;
  4. usingSystem.IO;
  5.  
  6. namespace ConsoleApplication1
  7. {
  8.     class Program
  9.     {
  10.         staticvoid Main(string[] args)
  11.         {
  12.             string filename ="c:\sharpertutorials\orders.csv";
  13.             string orders =string.Empty;
  14.             string data =string.Empty;
  15.             StreamReader myFile =null;
  16.  
  17.             try
  18.             {
  19.                 myFile =new StreamReader(filename);
  20.                 orders = myFile.ReadToEnd();
  21.  
  22.                 data = orders.Substring(01);
  23.             }
  24.             catch(DirectoryNotFoundException ex)
  25.             {
  26.                 Console.WriteLine("Sorry, the path to '"+ filename +"' does not exist. Please correct the error and try again.");
  27.             }
  28.             catch(FileNotFoundException ex)
  29.             {
  30.                 Console.WriteLine("Sorry, the file '"+ filename +"' does not exist. Please create the file and try again.");
  31.             }
  32.             finally
  33.             {
  34.                 myFile.Close();
  35.             }
  36.  
  37.             Console.WriteLine(orders);
  38.         }
  39.     }
  40. }
There we have a complete functioning try...catch...finally block. Hopefully you can see how and why this is an essential part of trouble free programming and how it can be used to avoid unhelpful and meaningless error messages.

Raising or Throwing an Exception

Exceptions can be manually thrown in your code, either for testing or to signal a fault that needs to be dealt with. It is important to validate inputs and notify errors, especially when dealing with untrusted code - such as that developed by another programmer. In this example the scenario is that we are developing a class to handle mathematical operations. Another developer will be using this class in their program. For simplicity I have included both classes in the same project.
  1. usingSystem;
  2. usingSystem.Collections.Generic;
  3. usingSystem.Text;
  4. usingSystem.IO;
  5.  
  6. namespace ConsoleApplication1
  7. {
  8.     // Writen by us
  9.     staticclass MyMathClass
  10.     {
  11.         publicstaticdecimal Divide(int x, int y)
  12.         {
  13.             return x / y;
  14.         }
  15.     }
  16.  
  17.  
  18.     // Writen by somebody else    
  19.     class Program
  20.     {
  21.         staticvoid Main(string[] args)
  22.         {
  23.             Console.WriteLine(MyMathClass.Divide(100));
  24.         }
  25.     }
  26. }
Here we can see that our class and method are fine. It will divide the first parameter by the second. What can go wrong? Well the other programmer is an "untrusted" source. They can pass in a valid number that causes our code to crash. In this example a divide by zero error. Since our code is the one that crashed, the fault and blame is ours.
Exception and Error Handling
What we can do to avoid this is validate the inputs and raise an exception that will be handled by their code. This means that if they pass in invalid data to our method, their code is at fault leaving us blame free. Raising an exception is done using the throw keyword. For a list of available exceptions you can throw, please click on Debug menu -> Exceptions... or press Ctrl+D, E. You can also create your own exceptions in the next section.
You can either throw a new exception or use the default message, or you can specify a more specific error message in the constructor, as shown below.
  1. usingSystem;
  2. usingSystem.Collections.Generic;
  3. usingSystem.Text;
  4. usingSystem.IO;
  5.  
  6. namespace ConsoleApplication1
  7. {
  8.     // Writen by us
  9.     staticclass MyMathClass
  10.     {
  11.         publicstaticdecimal Divide(int x, int y)
  12.         {
  13.             if(==0)
  14.                 thrownew ArgumentOutOfRangeException("Parameter y cannot be 0!");
  15.            
  16.             return x / y;
  17.         }
  18.     }
  19.    
  20.    
  21.     // Writen by somebody else    
  22.     class Program
  23.     {
  24.         staticvoid Main(string[] args)
  25.         {
  26.             Console.WriteLine(MyMathClass.Divide(100));
  27.         }
  28.     }
  29. }
Now when the program is run the calling method will receive an ArgumentOutOfRangeException message with our custom error message.

Custom Exceptions

If there is not a predefined exception that suits your needs, you can easily create a new custom exception by simply creating a class that inherits from any of the predefined exception classes or the base Exception class. They can be used in the same way as a normal exception.
  1. class myException :System.Exception
  2. {
  3. }
  4.  
  5. class Program
  6. {
  7.   staticvoid Main()
  8.   {
  9.     thrownew myException("Custom Exception");
  10.   }
  11. }

Exception Guidelines

When throwing exceptions you should avoid exceptions for normal or expected cases. These should be handled through proper program logic.
Never create and throw objects of class Exception, it's much better to throw exceptions of the most specific class possible as it will give you greater flexibility in catching the exception and handling it. Should an unhandled exception occur, the more specific exception class will give you a better idea of where to start debugging. You should also include a description string in an Exception object with details about the error.
When catching exceptions, arrange catch blocks from specific to general otherwise the general exception will always be caught and the specific catch blocks will never be processed. Do not let exceptions go unhandled in the Main method as this will cause the application to crash and the user will not be able to recover from the error.
Never use an empty catch block - what is the point?
  1. try
  2. {
  3.   x =new StreamReader(filename);
  4. }
  5. catch
  6. {
  7. }
  8.  
  9. Console.WriteLine(x.ReadToEnd());

More Tutorials in C# Programming

  1. What is C#?
  2. Your First Console Application
  3. Data Types, Variables and Casting
  4. Introducing Methods and the Main() Method
  5. Flow Control and Entry Points
  6. Using the Visual Studio Debugger
  7. Using Method Parameters and Return Values
  8. Access Modifiers and Scope
  9. Introducing Classes and Structs
  10. What are Namespaces?
  11. Interfaces and Classes
  12. Conditional Statements
  13. Looping and Iteration in C#
  14. Using Arrays and Lists in C#
  15. Constants and Read Only Variables
  16. Advanced Data Types
  17. Error and Exception Handling in C# ⇦ You Are here
  18. Using Properties and Indexers
  19. Using Recursion in C#
  20. Event Handling and Delegates
  21. Method Overloading and Overriding
  22. C# Operator List
  23. Class Inheritance
  24. Class Abstraction and Encapsulation
  25. Aggregation and Advanced Scope Techniques
  26. Class and Method Attributes Explained
  27. Class Constructors and Destructors
  28. Polymorphism in C#
  29. Boxing and Unboxing Value Types in C#
  30. Operator Overloading
  31. Creating Multithreaded Applications with C#
  32. Unsafe Code Execution in Microsoft .Net
  33. Generic Variables
  34. XML Serialization and Deserialization
  35. C# String Formatting Examples

No comments:

Post a Comment