Exception handling allows programmers to deal with unexpected situations that may occur in programs. As an example, consider opening a file using the
StreamReader class in the
System.IO namespace. To see what kinds of exceptions this class may throw, you can hover the cursor over the class name in Visual Studio. For instance, you may see the
System.IO exceptions
FileNotFoundException and
DirectoryNotFoundException. If any of those exceptions occurs, the program will terminate with an
error message.
using System;
using System.IO;
class ErrorHandling
{
static void Main()
{
// Run-time error
StreamReader sr = new StreamReader("missing.txt");
}
}
Try-Catch Statement
To avoid crashing the program, the exceptions must be caught using a
try-catch statement. This statement consists of a
try block containing the code that may cause the exception and one or more
catch clauses. If the
try block successfully executes, the program will then continue running after the
try-catch statement
. However, if an exception occurs, the execution will then be passed to the first
catch block able to handle that exception type.
try {
StreamReader sr = new StreamReader("missing.txt");
}
catch {
Console.WriteLine("File not found");
}
Catch Block
Since the previous
catch block
is not set to handle any specific exception, it will catch all of them. This is equivalent to catching the
System.Exception class, because all exceptions derive from this class.
To catch a more specific exception, that
catch block needs to be placed before more general exceptions.
catch (FileNotFoundException) {}
catch (Exception) {}
The
catch block can optionally define an exception object that can be used to obtain more information about the exception, such as a description of the error.
catch (Exception e) {
Console.WriteLine("Error: " + e.Message);
}
Exception Filters
Exception
filters were added in C# 6.0 and allow
catch blocks to include conditions. The condition is appended to the
catch block using the
when keyword. A matched exception will then only be caught if the condition evaluates to
true, as in the following example.
try {
StreamReader sr = new StreamReader("missing.txt");
}
catch (FileNotFoundException e)
when (e.FileName.Contains(".txt")) {
Console.WriteLine("Missing file: " + e.FileName);
}
When using exception filters, the same exception type may appear in multiple
catch clauses. Additionally, there are scenarios when a more general exception can be placed before more specific ones. In the next example, all exceptions are logged by calling a logging method as an exception filter. Because the method returns
false, the general exception is not caught and thereby allows for another
catch block to handle the exception.
using System;
using System.IO;
static class ErrorHandling
{
// Extension method
public static bool LogException(this Exception e)
{
Console.Error.WriteLine($"Exception: {e}");
return false;
}
static void Main()
{
try {
var sr = new StreamReader("missing.txt");
}
catch (Exception e) when (LogException(e)) {
// Never reached
}
catch (FileNotFoundException) {
// Actual handling of exception
}
}
}
Finally Block
As the last clause in the try-catch statement, a finally block
can be added. This block is used to clean up certain resources allocated in the try block. Typically, limited system resources and graphical components need to be released in this way once they are no longer needed. The code in the finally block will always execute, whether or not there is an exception. This will be the case even if the try block ends with a jump statement, such as return.
In the example used previously, the file opened in the
try block should be closed if it was successfully opened. This is done properly in the next code segment. To be able to access the
StreamReader object from the
finally clause, it must be declared outside of the
try block. Keep in mind that if you forget to close the stream, the garbage collector will eventually close it for you, but it is good practice to do it yourself.
try {
sr = new StreamReader("missing.txt");
}
catch (FileNotFoundException) {}
finally {
if (sr != null) sr.Close();
}
The previous statement is known as a
try-catch-finally statement. The
catch block may also be left out to create a
try-finally statement. This statement will not catch any exceptions. Instead, it will ensure the proper disposal of any resources allocated in the
try block. This can be useful if the allocated resource does not throw any exceptions. For instance, such a class would be
Bitmap in the
System.Drawing namespace.
using System.Drawing;
// ...
Bitmap b = null;
try {
b = new Bitmap(100, 50);
System.Console.WriteLine(b.Width); // "100"
}
finally {
if (b != null) b.Dispose();
}
Note that when using a Console Project a reference to the System.Drawing assembly needs to be manually added for those members to be accessible. To do so, right-click the References folder in the Solution Explorer window and select Add Reference. Then from Assemblies ➤ Framework, select the System.Drawing assembly and click OK to add its reference to your project.
Using Statement
The
using statement
provides a simpler syntax for writing the
try-finally statement. This statement starts with the
using keyword followed by the resource to be acquired, specified in parentheses. It then includes a code block in which the obtained resource can be used. When the code block finishes executing, the
Dispose method of the object is automatically called to clean it up. This method comes from the
System.IDisposable interface, so the specified resource must implement this interface. The following code performs the same function as the one in the previous example, but with fewer lines of code.
using System.Drawing;
// ...
void using (Bitmap b = new Bitmap(100, 50)) {
System.Console.WriteLine(b.Width); // "100"
} // disposed
C# 8.0 simplified resource management further by allowing for using declarations. This removes the need for the curly brackets as the resource handler will automatically be disposed of when it goes out of scope.
void MyBitmap()
{
using Bitmap b = new Bitmap(100, 50);
System.Console.WriteLine(b.Height); // "50"
} // disposed
Throwing Exceptions
When a situation occurs that a method cannot recover from, it can generate an exception to signal the caller that the method has failed. This is done using the
throw keyword followed by a new instance of a class deriving from
System.Exception.
static void MakeError()
{
throw new System.DivideByZeroException("My Error");
}
The exception will then propagate up the caller stack until it is caught. If a caller catches the exception but is not able to recover from it, the exception can be rethrown using only the
throw keyword. If there are no more
try-catch statements, the program will stop executing and display the error message.
static void Main()
{
try {
MakeError();
}
catch {
throw; // rethrow error
}
}
As a statement, the
throw keyword cannot be used in contexts that require an expression, such as inside a ternary statement. C# 7.0 changed this by allowing
throw to also be used as an expression. This expands the locations from which exceptions may be thrown, such as inside the following
null-coalescing expression.
using System;
class MyClass
{
private string _name;
public string name
{
get => _name;
set => _name = value ?? throw new
ArgumentNullException(nameof(name)+" was null");
}
static void Main()
{
MyClass c = new MyClass();
c.name = null; // exception: name was null
}
}
Note the use of the nameof expression here, which was introduced in C# 6.0. This expression turns the symbol inside the parentheses into a string. The benefit of this shows itself if the property is renamed, as the IDE can then find and rename this symbol. This would not be the case if a string had been used instead.