Since we now know a bit about C# classes, we can learn how to write code that handles unexpected situations and errors. This process is called exception handling, and is a part of all but the most basic C# programs.
Let's learn about exceptions and how C# allows us to handle them!
The Sample Solution
Exceptions
An exception is an unexpected error that happens while a C# program is running. The C# runtime will transform an exception into an instance of a class, and we can use that instance to try to debug why the error happened.
All instances of an exception in C# must inherit from the class System.Exception
. When an exception is encountered while a program is running, we say it has been thrown.
C# includes a huge variety of built-in exception classes. For example, if we attempt to divide by zero, an instance of System.DivideByZeroException
will be thrown.
public class Program
{
static void Main(string[] args)
{
int zero = 0;
int two = 2;
int result = two / zero; //Throws System.DivideByZeroException
}
}
Understanding Exceptions
When an exception is thrown, the C# compiler will include information in that exception instance which is helpful when attempting to find out where the exception was thrown from. This includes:
- Stack Trace - The classes and methods that were called in order to execute the line of code that threw the exception. We use this to trace back and find out what kinds of arguments or method calls might have led to this exception being thrown.
- Error Message - A short message explaining what the exception was.
- Inner Exceptions - Exceptions can be wrapped in other exceptions. When this happens, the innermost exception (i.e. the exception that does not have any further inner exceptions) is often the most useful for debugging.
Handling Thrown Exceptions
If we have a block or line of code that we know might throw an exception at runtime, we can wrap it using the try
keyword. Any exception which is caught in the try
block can then be processed in a corresponding catch
block.
try
{
var two = 2;
var zero = 0;
var result = two / zero; //Will throw DivideByZeroException
CallOtherMethod(); //Never invoked
}
catch(Exception ex)
{
//Code that will "handle" the exception.
}
Execution of the code in the try
block stops when an exception is encountered. In the above code, the method CallOtherMethod()
will never be invoked, because the line before it will always throw an exception.
The catch
keyword allows us to handle thrown exceptions. "Handling" an exception can mean many things. For example, we might want to log that the exception occurred in an error-logging system, redirect the code execution to a different method or block, or some combination of the two.
try
{
var two = 2;
var zero = 0;
var result = two / zero; //Will throw DivideByZeroException
CallOtherMethod(); //Never invoked
}
catch(Exception ex)
{
var errorLogger = new MyErrorLogger();
errorLogger.Log(ex);
}
Swallowing Exceptions
Choosing to do nothing with a caught exception is called "swallowing" the exception and is generally considered bad practice:
try
{
var result = two / zero; //Will throw DivideByZeroException
}
catch (Exception ex)
{
//Swallowing exceptions is a bad practice!
}
However, something being bad practice does not mean that we should never do it. It means we should try not to do it, or only do it in specific situations where we know and understand the risks or complications that might result.
In the case of swallowing exceptions, the primary risk is that errors might occur, and because they were swallowed, we will have no idea that they even happened.
Multiple Catch Blocks
We can also catch
specific kinds of exceptions and execute different code blocks for each. The most-specific exception needs to be listed first, and the least-specific goes last.
try
{
//Code that might throw an exception
}
catch (DivideByZeroException ex)
{
//Handle the divide by zero situation
}
catch (ArgumentException ex)
{
//Handle the argument exception situation
}
catch (Exception ex)
{
//Handle all other exceptions
}
Finally Blocks
Before the execution of a try
catch
block ends, the C# compiler will check for the existence of a finally
block. Code in a finally
block will execute whether or not an exception is thrown.
try
{
//Code that might throw an exception.
}
catch (Exception ex)
{
//Code that is only executed if an exception is thrown.
}
finally
{
//Code that is executed whether or not an exception is thrown.
}
A common situation in which we use a finally
clause is to clean up our code. We might do that in situations where we are reading external files, so as not to cause a StackOverflowException
or OutOfMemoryException
.
In order to read a file from a local device, you must open a stream to the file. The stream remains open as long as we do not explicitly close it. We could use a finally
block to close the stream whether or not an exception was thrown:
System.IO.FileStream file = null;
System.IO.FileInfo fileInfo = null;
try
{
fileInfo = new System.IO.FileInfo("C:\\path\\to\\file.txt");
file = fileInfo.OpenWrite();
file.WriteByte(0xF);
}
catch(Exception e)
{
//Handle the exception
Console.WriteLine(e.Message);
}
finally
{
if (file != null)
{
file.Close(); //Close the file stream
}
}
We would do similar things in situations where we are reading from a database or datastore.
Throwing Exceptions
We can also force the C# compiler to throw an exception using the throw
keyword.
if(someCondition)
{
//Normal condition
}
else //Error Condition
{
throw new Exception("Exception message goes here!");
}
We commonly use this in situations where the program is now in an error state, and we need to stop execution before something worse happens. You might use this in if
/else
blocks or other decision statements.
One common scenario is to throw a NotImplementedException
for methods that do not have their implementation written yet:
public string Method()
{
throw new NotImplementedException();
}
Note that the above method will not cause a compilation error, despite the fact that the method has a return type and is missing the return
keyword.
Custom Exceptions
We can also create our own custom exception classes, such as this one:
public class CustomException : Exception
{
public CustomException(string message) { /*...*/ }
}
Custom exception classes must inherit from either System.Exception
or another class which inherits from System.Exception
. Then, because of polymorphism, an instance of our custom exception class can be treated as though it is of type System.Exception
.
We can then throw instances of our custom exception using the throw
keyword:
public static void MyMethod()
{
throw new CustomException("This is a message!");
}
Re-Throwing Exceptions
There may be situations in which our catch
block has caught an exception, but needs to throw
it so that some code block or try
catch
block elsewhere in the code can handle it.
There are two ways of doing this. The first is to simply use the throw
keyword at the end of the catch
block.
try
{
//Code that might throw an exception
}
catch(Exception ex)
{
//Handle the exception
throw;
}
Doing this preserves the original stack trace so that it can be viewed when the exception is caught and handled later.
You can also "re-throw" the exception variable:
try
{
//Code that might throw an exception
}
catch(Exception ex)
{
//Handle the exception
throw ex; //This is different!
}
However, if we do this, the stack trace is reset to start at this line of code; the stack trace that was originally in the exception is lost. Therefore, we normally prefer the throw
method over the throw ex
method.
Treat Exceptions As Exceptional
The key thing to remember about exceptions is that they are not normal situations and should not be treated as a normal part of the code flow.
For example, we should not throw an exception merely to reach another part of the code (e.g. treating an exception like a decision statement such as if
or switch
). Exceptions are not decision points.
Also, exceptions should not be used as a return type for methods; they are not intended to be used in this way.
Exceptions are exceptional, and should be treated as such.
Glossary
- Exception - An unexpected error which occurs while our program is executing.
- Exception handling - The process by which our code generates and handles exceptions.
- Thrown - When an exception occurs in our program, we say that the program has "thrown" the exception.
- Handle - When dealing with exceptions, the code that does something with the thrown exception (e.g. logging it, changing the code that will be executed, etc.) is said to "handle" said exception.
New Keywords
try
- Specifies a block of code that could throw an exception, where that exception will now be handled.catch
- Specifies a kind of exception that will be "caught" and processed.finally
- Specifies a code block that will be executed regardless of whether or not an exception was thrown.throw
- Throws an exception.
Summary
An exception in C# is an unexpected error encountered at while a program is executing. Exceptions are thrown by the program, and can be caught and processed in try
/catch
blocks. Optional finally
blocks execute code regardless of exceptions thrown. We can throw exceptions explicitly using the throw
keyword, and most importantly, exceptions are exceptional and should be treated as such.
This post is a basic introduction to exceptions and exception handling; more details can be found in the official C# documentation.
If you have any questions, please ask them in the comments below and I'd be happy to help clear things up.
In the next part of our C# in Simple Terms series, we will expand on our knowledge of types and type handling by discussing arrays and collections. Check it out here:
Happy Coding!