Exceptions
It is inevitable that at some point in the process of developing an application an error will occur. No matter how good of a programmer you are, errors will happen.
Exceptions are special types of errors, generated by the operating system or by using someone else's code. Newer programmers view exceptions as a negative part of programming, because when an exception is not handled your program will abnormally end. Exceptions are similar to compiler errors in that they aid a programmer into developing better code. To debug and fix exceptions, you must understand:
- what exceptions are.
- how they affect the flow of code.
- how to gain information from the exception.
- how to handle an exception.
- the requirements for how the exception should be handled.
Understanding Exceptions
Although exceptions are a tool for programmers to make their code better, they are not desired. Having your program end abnormally will never be a requirement of any application you develop.
Exceptions are errors that only occur during the execution of a program. As previously mentioned, if an exception is not handled, the execution of the program will abnormally end.
Exceptions are generated when an illegal operation takes place. An example of this would be to perform division by zero. Exceptions also occur when invoking methods. Usually methods written by another programmer. An example of this would be trying to parse non-numeric data to a numeric type.
What is an Exception?
An exception is an object. This means that an exception is an instance of a class. All exceptions derive from the base class System.Exception
. There are many exception types that are defined in the framework of most OOP languages. All these exception types will derive directly or indirectly from the Exception
class.
Flow of Code
When an exception happens, two things take place:
- An instance of an exception is created.
- The exception is thrown down the Execution Call Stack.
When an exception is thrown down the method call stack, the normal flow of the code does not happen. This means that when a exception occurs, no code will execute. As the exception traverses down the call stack, the run-time is looking for the exception to be handled. If the exception is handled at some point in the execution call stack, the flow of code is regained. If the run-time makes it to the bottom of the execution call stack and does not find code to handle the exception, the program will terminate.
Execution Call Stack
The Execution Call Stack, or just call stack, keeps track of each active method and where a method should return to when it finishes executing. When a method is invoked (called), it is added (pushed on) to the call stack. The first method on the call stack is always the Main method. When a method completes, the method is removed from (popped off) the call stack, and execution continues where the previous method left off. The program ends when the last method (Main) is removed from the call stack.
This sample program has several methods. Each method has at least two statements. The first statement prints a message to indicate the method has been added to the stack. The last statement prints a message to indicate the method has been removed from the stack.
internal class Program
{
static void Main(String[] args)
{
Console.WriteLine("Main - Added to the call stack.");
MethodB();
MethodC();
MethodD();
MethodE();
Console.WriteLine("Main - Removed from the call stack.");
}
public static void MethodA(String message)
{
Console.WriteLine("MethodA - Added to the call stack.");
Console.WriteLine("MethodA - Removed from the call stack.");
}
public static void MethodB()
{
Console.WriteLine("MethodB - Added to the call stack.");
Console.WriteLine("MethodB - Removed from the call stack.");
}
public static void MethodC()
{
Console.WriteLine("MethodC - Added to the call stack.");
MethodB();
Console.WriteLine("MethodC - Removed from the call stack.");
}
public static String MethodD()
{
Console.WriteLine("MethodD - Added to the call stack.");
MethodC();
Console.WriteLine("MethodD - Removed from the call stack.");
return "Hello World";
}
public static void MethodE()
{
Console.WriteLine("MethodE - Added to the call stack.");
MethodA(MethodD());
Console.WriteLine("MethodE - Removed from the call stack.");
}
}
Output:
Main - Added to the call stack.
MethodB - Added to the call stack.
MethodB - Removed from the call stack.
MethodC - Added to the call stack.
MethodB - Added to the call stack.
MethodB - Removed from the call stack.
MethodC - Removed from the call stack.
MethodD - Added to the call stack.
MethodC - Added to the call stack.
MethodB - Added to the call stack.
MethodB - Removed from the call stack.
MethodC - Removed from the call stack.
MethodD - Removed from the call stack.
MethodE - Added to the call stack.
MethodD - Added to the call stack.
MethodC - Added to the call stack.
MethodB - Added to the call stack.
MethodB - Removed from the call stack.
MethodC - Removed from the call stack.
MethodD - Removed from the call stack.
MethodA - Added to the call stack.
MethodA - Removed from the call stack.
MethodE - Removed from the call stack.
Main - Removed from the call stack.
This simple application demonstrates the call stack. You can see that the main method is the first method in the call stack and is not removed until the very end.
Each time a method is invoked, it becomes active and is added to the call stack and is not removed until all other active methods added after it have been completed and removed.
Unhandled Exceptions
When an exception occurs, it will be generated (thrown) in the method currently at the top of the stack. Execution of the code will stop. The run-time will evaluate each method in the call stack to determine if the exception is handled. If it is handled, execution of code will continue at that point in the call stack. If the exception is not handled at any point in the call stack, the program will terminate.
If the implementation of MethodB()
is updated:
public static void MethodB()
{
Console.WriteLine("MethodB - Added to the call stack.");
// This line will generate an exception
double.Parse("Hello");
Console.WriteLine("MethodB - Removed from the call stack.");
}
Output:
Main - Added to the call stack.
MethodB - Added to the call stack.
Unhandled Exception: System.FormatException: Input string was not in a correct format.
at System.Number.ParseDouble(String value, NumberStyles options, NumberFormatInfo numfmt)
at System.Double.Parse(String s)
at ADEV.Module2.App.Program.MethodB() in C:\source\ADEV.Module2.App\Program.cs:line 41
at ADEV.Module2.App.Program.Main(String[] args) in C:\source\ADEV.Module2.App\Program.cs:line 15
You can see from the output of this program, that the program does not end properly. Had it ended properly, you would see the Main()
method removed from the call stack. You can also see that MethodB()
also did not complete properly, as it too was not removed from the call stack.
Exceptions interrupt the normal flow of a program.
The program ends with information printed about the exception that was generated. This information can be intimidating to read at first. The more mistakes you make, the more times you’ll read it. The more you read it, the better you’ll get at understanding it.
Unhandled Exception: System.FormatException: Input string was not in a correct format.
at System.Number.ParseDouble(String value, NumberStyles options, NumberFormatInfo numfmt)
at System.Double.Parse(String s)
at ADEV.Module2.App.Program.MethodB() in C:\source\ADEV.Module2.App\Program.cs:line 41
at ADEV.Module2.App.Program.Main(String[] args) in C:\source\ADEV.Module2.App\Program.cs:line 15
You can see that the type of exception was FormatException
. The data that caused the exception was the String "Hello"
. The text after this line displays the stack trace. The stack trace shows us the methods that were called, in the order they were added to the call stack, from main to the method where the exception was generated.
As your programs become more complicated, the stack trace is essential information for fixing defects.
Unhandled Exceptions In Visual Studio
When executing your program in Visual Studio, unhandled exceptions will stop the execution of the program and the statement that caused the exception is highlighted.
The callout box seen in the image above, shows the type of exception, exception message, and the stack trace. Read all of this before stopping the execution of the program.
Handling Exceptions
An exception is handled using a try...catch
structure. Generally speaking, you would put the statement that would potentially generate the exception within a try
block. If the exception is generated, the exception is caught by the catch
statement and execution of the code continues from the catch block.
public static void MethodB()
{
Console.WriteLine("MethodB - Added to the call stack.");
try
{
// This line will generate an exception
double.Parse("Hello");
}
catch (FormatException)
{
Console.WriteLine("Failed to parse the data to a double.");
}
Console.WriteLine("MethodB - Removed from the call stack.");
}
Tip
In almost all cases, it is necessary to include the exception type in the catch
statement. Although it can be omitted, it would catch all exceptions. This may sound great in theory, but would hide potential problems in your code.
Output:
Main - Added to the call stack.
MethodB - Added to the call stack.
Failed to parse the data to a double.
MethodB - Removed from the call stack.
MethodC - Added to the call stack.
MethodB - Added to the call stack.
Failed to parse the data to a double.
MethodB - Removed from the call stack.
MethodC - Removed from the call stack.
MethodD - Added to the call stack.
MethodC - Added to the call stack.
MethodB - Added to the call stack.
Failed to parse the data to a double.
MethodB - Removed from the call stack.
MethodC - Removed from the call stack.
MethodD - Removed from the call stack.
MethodE - Added to the call stack.
MethodD - Added to the call stack.
MethodC - Added to the call stack.
MethodB - Added to the call stack.
Failed to parse the data to a double.
MethodB - Removed from the call stack.
MethodC - Removed from the call stack.
MethodD - Removed from the call stack.
MethodA - Added to the call stack.
MethodA - Removed from the call stack.
MethodE - Removed from the call stack.
Main - Removed from the call stack.
It is important to understand the requirements of the code you are writing, as this determines where the try...catch
block is coded. The try...catch
block is always coded where the exception is to be handled.
In this updated version of the program, the exception is being handled in the Main()
method, which is lower in the call stack.
internal class Program
{
static void Main(String[] args)
{
Console.WriteLine("Main - Added to the call stack.");
try
{
MethodB();
MethodC();
MethodD();
MethodE();
}
catch (FormatException)
{
Console.WriteLine("Failed to parse the data to a double.");
}
Console.WriteLine("Main - Removed from the call stack.");
}
public static void MethodA(String message)
{
Console.WriteLine("MethodA - Added to the call stack.");
Console.WriteLine("MethodA - Removed from the call stack.");
}
public static void MethodB()
{
Console.WriteLine("MethodB - Added to the call stack.");
// This line will generate an exception
double.Parse("Hello");
Console.WriteLine("MethodB - Removed from the call stack.");
}
public static void MethodC()
{
Console.WriteLine("MethodC - Added to the call stack.");
MethodB();
Console.WriteLine("MethodC - Removed from the call stack.");
}
public static String MethodD()
{
Console.WriteLine("MethodD - Added to the call stack.");
MethodC();
Console.WriteLine("MethodD - Removed from the call stack.");
return "Hello World";
}
public static void MethodE()
{
Console.WriteLine("MethodE - Added to the call stack.");
MethodA(MethodD());
Console.WriteLine("MethodE - Removed from the call stack.");
}
}
You can see from the output below that the result is very different from when the exception was handled in MethodB()
.
Output:
Main - Added to the call stack.
MethodB - Added to the call stack.
Failed to parse the data to a double.
Main - Removed from the call stack.
If the code you are trying to execute has the potential to generate multiple types of exceptions, multiple catch
statements can be defined beneath the try
block.
Referencing Handled Exceptions
If you need to obtain information about a handled exception, you can declare a variable to reference the exception.
Note
The scope of the variable is limited to the catch
block.
Anticipating Exceptions
By reading the documentation for a method you are calling, you can determine if there is a potential for an exception to occur. If a method generates an exception, a section with the heading "Exceptions" will exist. The documentation will list the type of exceptions that are thrown and what circumstances need to occur for the exception to be thrown.
System exceptions are also documented, but can be found in explanations about how the language works. For example, the DivideByZeroException
is generated when using the division (/
) operator and the second operand is a zero. This exception doesn't happen when you invoke a method.
Common .NET Exception Types
Exceptions are types that derive from the System.Exception
class. The .NET Framework includes many exception types.
Here are some common exceptions:
- ArgumentException - Thrown when any argument is invalid. The
ArgumentException
is the base class of other argument exceptions (Ex.ArgumentOutOfRangeException
). - ArgumentNullException - Thrown when an argument is null.
- ArgumentOutOfRangeException - Thrown when an argument value is out of the intended range.
- FormatException - Thrown when an argument does not meet the format specifications intended by the method.
- IndexOutOfRangeException - Thrown when you attempt to index an array using an index that is less than zero or greater than the length - 1.
- InvalidCastException - Thrown when an explicit conversion from a base type or interface to a derived type fails.
- NullReferenceException - Thrown when a null reference is used.
- SystemException - Generated by the CLR.
Notable exception members:
- Message - Gets a message that describes the exception.
- StackTrace - Gets a string representation of the frames in the call stack.
Generating Exceptions
When developing code, mostly when developing data types, you will at some point need to generate an exception. Generating exceptions in your code helps another developer using your code by letting them know that they've done something incorrectly.
To demonstrate this, analyze the following code:
public class Person
{
private decimal amountOfMoney;
public decimal AddMoney(decimal amount)
{
this.amountOfMoney += amount;
return this.amountOfMoney;
}
}
If someone using the AddMoney(decimal)
method passes a negative numeric value as an argument, the method will not calculate the amountOfMoney
as intended. It should be noted that there is no way for the developer coding the AddMoney(decimal)
to prevent a negative numeric value as an argument.
You are probably already thinking that this can be avoided by using a selection, and you wouldn't be wrong.
public class Person
{
private decimal amountOfMoney;
public decimal AddMoney(decimal amount)
{
if(amount < 0)
{
}
this.amountOfMoney += amount;
return this.amountOfMoney;
}
}
The question is, what do you do when you've determined the amount
is a negative value?
Newer programmers would usually want to print an error message, telling the user that something went wrong.
There is a few problems with this approach:
- The intention of the message is not directed to the user, but to the developer using the method.
- If the application is not a Console Application, this message would not be visible and would not easily be detected.
- Seeing as though the message is being printed to the user, this message could be missed by the developer of the
Person
class. - Because the code execution would continue after this selection, the statement
amount = 0;
would be needed to avoid a logic error.
The correct approach would be to generate an exception. Since it is required that the amount
parameter be a zero or positive numeric value, a ArgumentOutOfRangeException
would be generated when the amount
is negative.
Generating an exception takes two steps:
- Generate an instance of an exception class.
throw
the exception instance.
public class Person
{
private decimal amountOfMoney;
public decimal AddMoney(decimal amount)
{
if(amount < 0)
{
ArgumentOutOfRangeException exception;
exception = new ArgumentOutOfRangeException("amount", "The argument cannot be less than zero.");
throw exception;
}
this.amountOfMoney += amount;
return this.amountOfMoney;
}
}
When the code execution performs the throw
statement, the flow of code is interrupted and the run-time will traverse the call stack to determine if the exception is handled.
Documentation
When a method or property contains code that potentially throws an exception, information about the exception must be documented.
When XML documentation is generated, Visual Studio does not typically parse the implementation of the method or property, only its declaration. Therefore, you must manually add the XML to the documentation for things like exceptions.
Exceptions are documented in an <exception>...</exception>
element.
The documentation will include only one exception element per exception type generated in the method or property.The exception element will include one attribute, called cref
, that is equal to the fully qualified name of the exception data type. The data within the element is the description of the situation(s) when that exception is thrown in the method or property. The text typically starts with "Thrown when".
Check out documentation samples