Error and Exception Handling in C#

A look at Error and Exception Handling in C# using try...catch blocks, custom exception classes and some debugging using breakpoints.

3,027 words, estimated reading time 12 minutes.
Introduction to Programming with C#

This article is part of a series of articles. Please use the links below to navigate between the articles.

  1. Learn to Program in C# - Full Introduction to Programming Course
  2. Introdution to Programming - C# Programming Fundamentals
  3. Introduction to Object Oriented Programming for Beginners
  4. Introduction to C# Object-Oriented Programming Part 2
  5. Application Flow Control and Control Structures in C#
  6. Guide to C# Data Types, Variables and Object Casting
  7. C# Collection Types (Array,List,Dictionary,HashTable and More)
  8. C# Operators: Arithmetic, Comparison, Logical and more
  9. Using Entity Framework & ADO.Net Data in C# 7
  10. What is LINQ? The .NET Language Integrated Query
  11. Error and Exception Handling in C#
  12. Advanced C# Programming Topics
  13. All About Reflection in C# To Read Metadata and Find Assemblies
  14. What Are ASP.Net WebForms
  15. Introduction to ASP.Net MVC Web Applications and C#
  16. Windows Application Development Using .Net and Windows Forms
  17. Assemblies and the Global Assembly Cache in C#
  18. Working with Resources Files, Culture & Regions in .Net
  19. The Ultimate Guide to Regular Expressions: Everything You Need to Know
  20. Introduction to XML and XmlDocument with C#
  21. Complete Guide to File Handling in C# - Reading and Writing Files

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 dividing 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 and 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 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.

Exception Handling by Catching an Exception

C#
using System;
using System.Collections.Generic;
using System.Text;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            int x = 0;
            int y = 0;
            y = 100 / x;
        }
    }
}

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.

C#
using System;
using System.Collections.Generic;
using System.Text;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            int x = 0;
            int y = 0;

            try
            {
                y = 100 / x;
            }
            catch
            {
                Console.WriteLine("There was an error but we caught it!");
                Console.WriteLine("Please enter a new number:");
                y = int.Parse(Console.ReadLine());
            }
        }
    }
}

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.

C#
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            string filename = "c:azuliadesignstutorialsdata.csv";
            StreamReader myFile = new StreamReader(filename);
            string orders = myFile.ReadToEnd();
            myFile.Close();

            Console.WriteLine(orders);
        }
    }
}

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.

C#
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            string filename = "c:azuliadesignstutorialsdata.csv";
            string orders = string.Empty;
            StreamReader myFile = null;
            
            try
            {
                myFile = new StreamReader(filename);
                orders = myFile.ReadToEnd();
                myFile.Close();
            }
            catch
            {
                Console.WriteLine("Sorry, an error has occurred.");
            }

            Console.WriteLine(orders);
        }
    }
}

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.

C#
namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            string filename = "c:azuliadesignstutorialsdata.csv";
            string orders = string.Empty;
            StreamReader myFile = null;
            
            try
            {
                myFile = new StreamReader(filename);
                orders = myFile.ReadToEnd();
                myFile.Close();
            }
            catch(DirectoryNotFoundException ex)
            {
                Console.WriteLine("Sorry, the path to '" + filename + "' does not exist. Please correct the error and try again.");
            }

            Console.WriteLine(orders);
        }
    }
}

Now when the program is run the user has 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.

C#
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            string filename = "c:azuliadesignstutorialsdata.csv";
            string orders = string.Empty;
            StreamReader myFile = null;

            try
            {
                myFile = new StreamReader(filename);
                orders = myFile.ReadToEnd();
                myFile.Close();
            }
            catch (DirectoryNotFoundException ex)
            {
                Console.WriteLine("Sorry, the path to '" + filename + "' does not exist. Please correct the error and try again.");
            }
            catch (FileNotFoundException ex)
            {
                Console.WriteLine("Sorry, the file '" + filename + "' does not exist. Please create the file and try again.");
            }

            Console.WriteLine(orders);
        }
    }
}

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.

C#
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            string filename = "c:azuliadesignstutorialsdata.csv";
            string orders = string.Empty;
            string data = string.Empty;
            StreamReader myFile = null;

            try
            {
                myFile = new StreamReader(filename);
                orders = myFile.ReadToEnd();

                data = orders.Substring(0, 1);

                myFile.Close();
            }
            catch (DirectoryNotFoundException ex)
            {
                Console.WriteLine("Sorry, the path to '" + filename + "' does not exist. Please correct the error and try again.");
            }
            catch (FileNotFoundException ex)
            {
                Console.WriteLine("Sorry, the file '" + filename + "' does not exist. Please create the file and try again.");
            }

            Console.WriteLine(data);
        }
    }
}

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.

C#
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            string filename = "c:azuliadesignstutorialsdata.csv";
            string orders = string.Empty;
            string data = string.Empty;
            StreamReader myFile = null;

            try
            {
                myFile = new StreamReader(filename);
                orders = myFile.ReadToEnd();

                data = orders.Substring(0, 1);
            }
            catch (DirectoryNotFoundException ex)
            {
                Console.WriteLine("Sorry, the path to '" + filename + "' does not exist. Please correct the error and try again.");
            }
            catch (FileNotFoundException ex)
            {
                Console.WriteLine("Sorry, the file '" + filename + "' does not exist. Please create the file and try again.");
            }
            finally
            {
                myFile.Close();
            }

            Console.WriteLine(orders);
        }
    }
}

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.

C#
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;

namespace ConsoleApplication1
{
    / Writen by us
    static class MyMathClass
    {
        public static decimal Divide(int x, int y)
        {
            return x / y;
        }
    }


    / Writen by somebody else    
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(MyMathClass.Divide(10, 0));
        }
    }
}

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.

C#
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;

namespace ConsoleApplication1
{
    / Writen by us
    static class MyMathClass
    {
        public static decimal Divide(int x, int y)
        {
            if (y == 0)
                throw new ArgumentOutOfRangeException("Parameter y cannot be 0!");
            
            return x / y;
        }
    }
    
    
    / Writen by somebody else    
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(MyMathClass.Divide(10, 0));
        }
    }
}

Now when the program is run the calling method will receive an ArgumentOutOfRangeException message with our custom error message.

Exception Handling with 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.

C#
class myException : System.Exception
{
}

class Program
{
  static void Main()
  {
    throw new myException("Custom Exception");
  }
}

Exception Handling 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?

C#
try
{
  var x = new StreamReader(filename);
}
catch
{
}

Console.WriteLine(x.ReadToEnd());

Using the Visual Studio Debugger

Visual Studio and the Express editions all have a very powerful debugging tool which features a step-through debugger, watch list and call stack among other features. These are all essential tools for debugging and testing applications.

The most basic feature of the debug environment is breakpoints. When the program is run in debug mode the application will pause and return to the editor when it hits a breakpoint. This will allow you to see what the program is doing at that stage of execution. You can step through the code to see where the flow of execution moves; you can inspect variables, view the call stack and evaluate expressions.

Breakpoints

Breakpoints can be set in several ways, the easiest of which is to click on the gutter (the grey area to the left of the code editor). This will make the line highlight red with a circle in the gutter. You can only set a breakpoint on a valid line of executable code. You can also right-click on the code line and set a breakpoint or you can use the Debug menu to add breakpoints. They can be removed in the same way as you added them.

Breakpoints in action
Breakpoints in action

When you run the program in debug mode (F5) the program will pause just before the breakpoint and return to the code editor window. From here you can now investigate the cause of the problem, either by interrogating the variables or stepping through the code. You can see the contents of a variable, object or class by hovering over the variable with the mouse. The editor will pop up a window containing detailed information about the object. We will cover interrogating variables more in the "Watches" section below.

Breakpoint variable investigation
Breakpoint variable investigation

You can step through the code in several different methods, you can step through line by line using F11, step over using F10 or step out using (Shift+F11).

Step Through: Every line of code executed will be debugged. When a method call is invoked the flow enters the method and returns to the calling line after it has completed.

Step Over: As above, however, you will not debug internal method calls. This is a better debug tool if you already know that a method is working and just weren't to call it without debugging.

Step Out: If you entered a method using Step Through, Step Out will return you to the point that the method was called.

Have a play with the debugger using this code:

C#
using System;

class Program
{
  static void Main()
  {
    int a = testMethod1();
    int b = testMethod2();
    Console.WriteLine(a + b);    
  }

  public int testMethod1()
  {
    int x = 1;
    int y = 2;
    return (x * y);
  }

  public int testMethod2()
  {
    int x = 5;
    int y = 10;
    return (x + y);
  }
}

Set a breakpoint on the call to testMethod1 in Main. Start debugging using F5 and step through the program. Have a go at investigating variables with the mouse. Step through the code and see how the flow moves into testMethod1, back to main and then into testMethod2.

Close the program and start debugging again, this time use the step-over and notice the difference. The debugger only stops on each line of Main, but steps over the code inside the methods.

Start debugging again (last time) and step into again. When you get inside testMethod1, use step out to return to the main method.

Conditional Breakpoints

By default, a breakpoint will stop program execution every time they are hit. You can, however, set a breakpoint to only stop when a certain condition is met. By right-clicking on the breakpoint, you can add conditions so that if the condition is not met then the debugger will not stop at the breakpoint. You can also tell the debugger to perform additional actions when a breakpoint is triggered, like displaying a message or running a macro. You can also view the "hit count" which is useful when debugging an iteration or recursive method.

Watches, Auto's and Locals

Watches, Auto's and Locals are windows that are available during debug. You can add a "watch" which will display the value of the selected variable in the watch's window and you can see how the value changes as you step through the code. This is particularly useful for debugging a recursive or iterative method as you can see how all the values interact, or look at the loop counter and so on.

Auto's and locals are automatically generated watch lists and contain the variables available in the current context.

One of the more useful features of auto's and watches is that you can modify the contents of a variable during debugging. When inspecting a variable you can right-click on it and select "Edit Value". Hitting enter will update the value and allow you to continue testing with a different value.

Debug Class

Microsoft .Net contains a class called Debug which you can use to log debug messages to the output window. These debug messages will only be available while the program is running under debug mode in the IDE. If it is compiled under release these debug messages are removed.

To use the Debug class, simply add "using System.Diagnostics;" to your uses section and call Debug.Write:

C#
using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Diagnostics;

namespace ConsoleApplication3
{
    class Program
    {
        static void Main(string[] args)
        {
            Image image = Bitmap.FromFile("c:breakpoints.png");

            if (image.Height > 100)
            {
                Debug.Write("Image height is too large");
            }
            else
            {

            }
        }
    }
}

In addition to Write, you call WriteIf, WriteLine and WriteLineIf. WriteIf and WriteLineIf will only write the debug message if the condition is met. WriteLine appends a line terminator to the message.

C#
using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Diagnostics;

namespace ConsoleApplication3
{
    class Program
    {
        static void Main(string[] args)
        {
            Image image = Bitmap.FromFile("c:breakpoints.png");

            Debug.WriteIf(image.Height > 100, "Image height is too large");
        }
    }
}
Debug.Write example
Debug.Write example

Immediate Window

The immediate window is a very powerful tool for debugging as it allows you to not only inspect variables and method return values but to also construct and execute statements. Virtually any single-line statement can be run, inspected and modified.

Visual Studio Immediate Window
Visual Studio Immediate Window

This can be particularly useful when trying to format a string, or counting indexes for substring as shown above. You can make quick changes easily to get the desired result without having to recompile.

Was this article helpful to you?
 

Related ArticlesThese articles may also be of interest to you

CommentsShare your thoughts in the comments below

If you enjoyed reading this article, or it helped you in some way, all I ask in return is you leave a comment below or share this page with your friends. Thank you.

There are no comments yet. Why not get the discussion started?

We respect your privacy, and will not make your email public. Learn how your comment data is processed.