Website development and design blog, tutorials and inspiration

Error and Exception Handling in C#

Error handling in C#

By , 8th November 2007 in C#

Critical errors are called Exceptions and they are raised whenever the compiler encounters a problem with a segment of code. An 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 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. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4.  
  5. namespace ConsoleApplication1
  6. {
  7. class Program
  8. {
  9. static void 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 an 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. The 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. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4.  
  5. namespace ConsoleApplication1
  6. {
  7. class Program
  8. {
  9. static void 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. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. using System.IO;
  5.  
  6. namespace ConsoleApplication1
  7. {
  8. class Program
  9. {
  10. static void Main(string[] args)
  11. {
  12. string filename = "c:sharpertutorialsorders.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
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. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. using System.IO;
  5.  
  6. namespace ConsoleApplication1
  7. {
  8. class Program
  9. {
  10. static void Main(string[] args)
  11. {
  12. string filename = "c:sharpertutorialsorders.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. static void Main(string[] args)
  6. {
  7. string filename = "c:sharpertutorialsorders.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
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. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. using System.IO;
  5.  
  6. namespace ConsoleApplication1
  7. {
  8. class Program
  9. {
  10. static void Main(string[] args)
  11. {
  12. string filename = "c:sharpertutorialsorders.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. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. using System.IO;
  5.  
  6. namespace ConsoleApplication1
  7. {
  8. class Program
  9. {
  10. static void Main(string[] args)
  11. {
  12. string filename = "c:sharpertutorialsorders.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(0, 1);
  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 an 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. The code in the finally block is always guaranteed to run so we can use that to close any files or connections.

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. using System.IO;
  5.  
  6. namespace ConsoleApplication1
  7. {
  8. class Program
  9. {
  10. static void Main(string[] args)
  11. {
  12. string filename = "c:sharpertutorialsorders.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(0, 1);
  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. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. using System.IO;
  5.  
  6. namespace ConsoleApplication1
  7. {
  8. // Writen by us
  9. static class MyMathClass
  10. {
  11. public static decimal Divide(int x, int y)
  12. {
  13. return x / y;
  14. }
  15. }
  16.  
  17.  
  18. // Writen by somebody else
  19. class Program
  20. {
  21. static void Main(string[] args)
  22. {
  23. Console.WriteLine(MyMathClass.Divide(10, 0));
  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.

Error and Exception Handling
Error and Exception Handling

What we can do to avoid this validates 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 to 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. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. using System.IO;
  5.  
  6. namespace ConsoleApplication1
  7. {
  8. // Writen by us
  9. static class MyMathClass
  10. {
  11. public static decimal Divide(int x, int y)
  12. {
  13. if (y == 0)
  14. throw new 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. static void Main(string[] args)
  25. {
  26. Console.WriteLine(MyMathClass.Divide(10, 0));
  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. static void Main()
  8. {
  9. throw new 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());
Comments

There are no comments for this post. Be the first!

Leave a Reply

Your email address will not be published.