Basic Error Handling with Exceptions

This section examines how to handle error reporting via a mechanism known as exception handling. With exception handling, a method can pass information about an error to a calling method without using a return value or explicitly providing any parameters to do so. Listing 5.25 with Output 5.10 contains a slight modification to Listing 1.16—the HeyYou program from Chapter 1. Instead of requesting the last name of the user, it prompts for the user’s age.

Listing 5.25: Converting a string to an int
public static void Main()
{
    string? firstName;
    string ageText;
    int age;
 
    Console.WriteLine("Hey you!");
 
    Console.Write("Enter your first name: ");
    firstName = Console.ReadLine();
 
    Console.Write("Enter your age: ");
    // Assume not null for clarity
    ageText = Console.ReadLine()!;
    age = int.Parse(ageText);
 
    Console.WriteLine(
        $"Hi { firstName }!  You are { age * 12 } months old.");
}
Output 5.10
Hey you!
Enter your first name: Inigo
Enter your age: 42
Hi Inigo!  You are 504 months old.

The return value from System.Console.ReadLine() is stored in a variable called ageText and is then passed to a method with the int data type, called Parse(). This method is responsible for taking a string value that represents a number and converting it to an int type.

Beginner Topic
42 as a String versus 42 as an Integer

C# requires that every non-null value have a well-defined type associated with it. Therefore, not only the data value but also the type associated with the data is important. A string value of "42", therefore, is distinctly different from an integer value of 42. The string is composed of the two characters 4 and 2, whereas the int is the number 42.

Given the converted string, the final System.Console.WriteLine() statement will print the age in months by multiplying the age value by 12.

But what happens if the user does not enter a valid integer string? For example, what happens if the user enters “forty-two”? The Parse() method cannot handle such a conversion. It expects the user to enter a string that contains only digits. If the Parse() method is sent an invalid value, it needs some way to report this fact back to the caller.

Trapping Errors

To indicate to the calling method that the parameter is invalid, int.Parse() will throw an exception. Throwing an exception halts further execution in the current control flow and jumps into the first code block within the call stack that handles the exception.

Since you have not yet provided any such handling, the program reports the exception to the user as an unhandled exception. Assuming there is no registered debugger on the system, Console applications will display the error on the console with a message such as that shown in Output 5.11.

Output 5.11
Hey you!
Enter your first name: Inigo
Enter your age: forty-two
Unhandled Exception: System.FormatException: Input string was
        not in a correct format.
    at System.Number.ThrowOverflowOrFormatException(...)
    at System.Number.ParseInt32(String s)
    at ...ExceptionHandling.Main()

Obviously, such an error is not particularly helpful. To fix this, it is necessary to provide a mechanism that handles the error, perhaps reporting a more meaningful error message back to the user.

This process is known as catching an exception. The syntax is demonstrated in Listing 5.26, and the output appears in Output 5.12.

Listing 5.26: Catching an Exception
public class ExceptionHandling
{
    public static int Main(string[] args)
    {
        string? firstName;
        string ageText;
        int age;
        int result = 0;
 
        Console.Write("Enter your first name: ");
        firstName = Console.ReadLine();
 
        Console.Write("Enter your age: ");
        // Assume not null for clarity
        ageText = Console.ReadLine()!;
 
        try
        {
            age = int.Parse(ageText);
            Console.WriteLine(
                $"Hi { firstName }! You are { age * 12 } months old.");
        }
        catch(FormatException)
        {
            Console.WriteLine(
                $"The age entered, { ageText }, is not valid."); 
            result = 1;
        }
        catch(Exception exception)
        {
            Console.WriteLine(
                $"Unexpected error: { exception.Message }");
            result = 1;
        }
        finally
        {
            Console.WriteLine($"Goodbye { firstName }");
        }
 
        return result;
    }
}
Output 5.12
Enter your first name: Inigo
Enter your age: forty-two
The age entered, forty-two, is not valid.
Goodbye Inigo

To begin, surround the code that could potentially throw an exception (age = int.Parse()) with a try block. This block begins with the try keyword. It indicates to the compiler that the developer is aware of the possibility that the code within the block might throw an exception, and if it does, one of the catch blocks will attempt to handle the exception.

One or more catch blocks (or the finally block) must appear immediately following a try block. The catch block header (see Advanced Topic: General Catch later in this chapter) optionally allows you to specify the data type of the exception. As long as the data type matches the exception type, the catch block will execute. If, however, there is no appropriate catch block, the exception will fall through and go unhandled as though there were no exception handling. The resultant control flow appears in Figure 5.1.

Figure 5.1: Exception-handling control flow

For example, assume the user enters “forty-two” for the age in the previous example. In this case, int.Parse() will throw an exception of type System.FormatException, and control will jump to the set of catch blocks. (System.FormatException indicates that the string was not of the correct format to be parsed appropriately.) Since the first catch block matches the type of exception that int.Parse() threw, the code inside this block will execute. If a statement within the try block threw a different exception, the second catch block would execute because all exceptions are of type System.Exception.

If there were no System.FormatException catch block, the System.Exception catch block would execute even though int.Parse throws a System.FormatException. This is because a System.FormatException is also of type System.Exception. (System.FormatException is a more specific implementation of the generic exception, System.Exception.)

The order in which you handle exceptions is significant. Catch blocks must appear from most specific to least specific. The System.Exception data type is least specific, so it appears last. System.FormatException appears first because it is the most specific exception that Listing 5.26 handles.

Regardless of whether control leaves the try block normally or because the code in the try block throws an exception, the finally block of code will execute after control leaves the try-protected region. The purpose of the finally block is to provide a location to place code that will execute regardless of how the try/catch blocks exit—with or without an exception. Finally blocks are useful for cleaning up resources, regardless of whether an exception is thrown. In fact, it is possible to have a try block with a finally block and no catch block. The finally block executes regardless of whether the try block throws an exception or whether a catch block is even written to handle the exception. Listing 5.27 demonstrates the try/finally block, and Output 5.13 shows the results.

Listing 5.27: Finally Block without a Catch Block
using System;
 
public class ExceptionHandling
{
    public static int Main()
    {
        string? firstName;
        string ageText;
        int age;
        int result = 0;
 
        Console.Write("Enter your first name: ");
        firstName = Console.ReadLine();
 
        Console.Write("Enter your age: ");
        // Assume not null for clarity
        ageText = Console.ReadLine()!;
 
        try
        {
            age = int.Parse(ageText);
            Console.WriteLine(
                $"Hi { firstName }! You are { age * 12 } months old.");
        }
        finally
        {
            Console.WriteLine($"Goodbye { firstName }");
        }
 
        return result;
    }
}
Output 5.13
Enter your first name: Inigo
Enter your age: forty-two
Unhandled Exception: System.FormatException: Input string was
        not in a correct format.
    at System.Number.ThrowOverflowOrFormatException(...)
    at System.Number.ParseInt32(String s)
    at ...ExceptionHandling.Main()
Goodbye Inigo

The attentive reader will have noticed something interesting here: The runtime first reported the unhandled exception and then ran the finally block. What explains this unusual behavior?

First, the behavior is legal because when an exception is unhandled, the behavior of the runtime is implementation defined—any behavior is legal! The runtime chooses this particular behavior because it knows before it chooses to run the finally block that the exception will be unhandled; the runtime has already examined all of the activation frames on the call stack and determined that none of them is associated with a catch block that matches the thrown exception.

As soon as the runtime determines that the exception will be unhandled, it checks whether a debugger is installed on the machine, because you might be the software developer who is analyzing this failure. If a debugger is present, it offers the user the chance to attach the debugger to the process before the finally block runs. If there is no debugger installed or if the user declines to debug the problem, the default behavior is to print the unhandled exception to the console and then see if there are any finally blocks that could run. Due to the “implementation-defined” nature of the situation, the runtime is not required to run finally blocks in this situation; an implementation may choose to do so or not.

note
If an exception goes unhandled before the process exits, the order of execution of the finally block, and whether it even executes, is implementation defined.
Guidelines
AVOID explicitly throwing exceptions from finally blocks. (Implicitly thrown exceptions resulting from method calls are acceptable.)
DO favor try/finally and avoid using try/catch for cleanup code.
DO throw exceptions that describe which exceptional circumstance occurred and, if possible, how to prevent it.
AdVanced Topic
Exception Class Inheritance

All objects thrown as exceptions derive from System.Exception.10 (Objects thrown from other languages that do not derive from System.Exception are automatically “wrapped” by an object that does.) Therefore, they can be handled by the catch(System.Exception exception) block. It is preferable, however, to include a catch block that is specific to the most derived type (e.g., System.FormatException), because then it is possible to get the most information about an exception and handle it less generically. In so doing, the catch statement that uses the most derived type can handle the exception type specifically, accessing data related to the exception thrown and avoiding conditional logic to determine what type of exception occurred.

Therefore, C# enforces the rule that catch blocks appear from most derived to least derived. For example, a catch statement that catches System.Exception cannot appear before a statement that catches System.FormatException because System.FormatException derives from System.Exception.

A method could throw many exception types. Table 5.2 lists some of the more common ones within the framework.

Table 5.2: Common Exception Types

Exception Type

Description

System.Exception

The “base” exception from which all other exceptions derive.

System.ArgumentException

Indicates that one of the arguments passed into the method is invalid.

System.ArgumentNullException

Indicates that a particular argument is null and that this is not a valid value for that parameter.

System.ApplicationException

To be avoided. The original idea was that you might want to have one kind of handling for system exceptions and another for application exceptions, which, although plausible, doesn’t actually work well in the real world.

System.FormatException

Indicates that the string format is not valid for conversion.

System.IndexOutOfRangeException

Indicates that an attempt was made to access an array or other collection element that does not exist.

System.InvalidCastException

Indicates that an attempt to convert from one data type to another was not a valid conversion.

System.InvalidOperationException

Indicates that an unexpected scenario has occurred such that the application is no longer in a valid state of operation.

System.NotImplementedException

Indicates that although the method signature exists, it has not been fully implemented.

System.NullReferenceException

Thrown when code tries to find the object referred to by a reference that is null.

System.ArithmeticException

Indicates an invalid math operation, not including divide by zero.

System.ArrayTypeMismatchException

Occurs when attempting to store an element of the wrong type into an array.

System.StackOverflowException

Indicates an unexpectedly deep recursion.

AdVanced Topic
General Catch

It is possible to specify a catch block that takes no parameters, as shown in Listing 5.28.

Listing 5.28: General Catch Blocks
// A previous catch clause already catches all exceptions
#pragma warning disable CS1058
// ...
        try
        {
            age = int.Parse(ageText);
            Console.WriteLine(
                $"Hi { firstName }! You are { age * 12 } months old.");
        }
        catch(FormatException exception)
        {
            Console.WriteLine(
                $"The age entered ,{ageText}, is not valid.");
            result = 1;
        }
        catch(Exception exception)
        {
            Console.WriteLine(
                $"Unexpected error: { exception.Message }");
            result = 1;
        }
        catch
        {
            Console.WriteLine("Unexpected error!");
            result = 1;
        }
        finally
        {
            Console.WriteLine($"Goodbye { firstName }");
        }

A catch block with no data type, called a general catch block, is equivalent to specifying a catch block that takes an object data type—for instance, catch(object exception){...}. For this reason, a warning is triggered stating that the catch block already exists; hence the #pragma warning disable directive.

Because all classes ultimately derive from object, a catch block with no data type must appear last.

General catch blocks are rarely used because there is no way to capture any information about the exception. In addition, C# doesn’t support the ability to throw an exception of type object. (Only libraries written in languages such as C++ allow exceptions of any type.)

Guidelines
AVOID general catch blocks and replace them with a catch of System.Exception.
AVOID catching exceptions for which the appropriate action is unknown. It is better to let an exception go unhandled than to handle it incorrectly.
Reporting Errors Using a throw Statement

C# allows developers to throw exceptions from their code, as demonstrated in Listing 5.29 and Output 5.14.

Listing 5.29: Throwing an Exception
public static void Main()
{
    try
    {
        Console.WriteLine("Begin executing");
        Console.WriteLine("Throw exception");
        throw new Exception("Arbitrary exception");
        // Catch 1
        Console.WriteLine("End executing");
    }
    catch(FormatException exception)
    {
        Console.WriteLine(
            "A FormatException was thrown");
    }
    // Catch 1
    catch(Exception exception)
    {
        Console.WriteLine(
            $"Unexpected error: { exception.Message }");
        // Jump to Post Catch
    }
    catch
    {
        Console.WriteLine("Unexpected error!");
    }
 
    // Post Catch
    Console.WriteLine(
        "Shutting down...");
}
Output 5.14
Begin executing
Throw exception...
Unexpected error:  Arbitrary exception
Shutting down...

As the comments in Listing 5.29 depict, throwing an exception causes execution to jump from where the exception is thrown into the first catch block (Catch 1) within the stack that is compatible with the thrown exception type.11 In this case, the second catch block handles the exception and writes out an error message. In Listing 5.29, there is no finally block, so following the WriteLine() of Catch 1 execution falls through to Post Catch, the Console.WriteLine(), statement following the try/catch block.

To throw an exception, it is necessary to have an instance of an exception. Listing 5.29 creates an instance using the keyword new followed by the type of the exception. Most exception types allow a message to be generated as part of throwing the exception, so that when the exception occurs, the message can be retrieved.

Sometimes a catch block will trap an exception but be unable to handle it appropriately or fully. In these circumstances, a catch block can rethrow the exception using the throw statement without specifying any exception, as shown in Listing 5.30.

Listing 5.30: Rethrowing an Exception
// ...
catch (Exception exception)
{
    Console.WriteLine(
        "Rethrowing unexpected error:  "
        + $"{ exception.Message }");
 
    throw;
}
// ...

In Listing 5.30, the throw statement is “empty” rather than specifying that the exception referred to by the exception variable is to be thrown. This illustrates a subtle difference: throw; preserves the call stack information in the exception, whereas throw exception; replaces that information with the current call stack information. For debugging purposes, it is usually better to know the original call stack. Of course, this is only allowed in a catch statement where the caught exception can be determined.

Guidelines
DO prefer using an empty throw when catching and rethrowing an exception, to preserve the call stack.
DO report execution failures by throwing exceptions rather than returning error codes.
DO NOT have public members that return exceptions as return values or an out parameter. Throw exceptions to indicate errors; do not use them as return values to indicate errors.
AVOID catching and logging an exception before rethrowing it. Instead, allow the exception to escape until it can be handled appropriately.
Reporting Null Argument Exceptions

When nullability reference types are enabled, the compiler will make a best effort to identify when there is the possibility of a nullable argument passed as a non-nullable argument. If you assign a possible null value to a non-nullable argument, the compiler will issue a warning stating that, you are attempting to convert a possible null value to a non-nullable argument. Assuming you address all such warning appropriately, the compiler will catch most of the cases. However, if the caller is outside of your control (such as from a library you didn’t write), the caller doesn’t turn on nullable reference types, ignores the warnings, or invokes the method from a C# 7.0 or earlier, there is nothing preventing a null argument even though the parameter is declared as non-nullable. For this reason, the best practice is to check public non-nullable reference types are not null. With .NET 6.0 you can use a conditional if is null check:

You can accomplish this in a single statement using the null coalescing assignment operator with a throw ArgumentNullException expression if the parameter value is null (See Listing 5.31).

Listing 5.31: Parameter Validation by throwing ArgumentNullException
httpsUrl = httpsUrl ??
    throw new ArgumentNullException(nameof(httpsUrl));
fileName = fileName??
    throw new ArgumentNullException(nameof(fileName));
 
 
// ...

With .NET 7.0, you can use the ArgumentNullException.ThrowIfNull() method (see Listing 5.32).

Listing 5.32: Parameter Validation With ArgumentException.ThrowIfNull()
ArgumentNullException.ThrowIfNull(httpsUrl);
ArgumentNullException.ThrowIfNull(fileName);
 
// ...

Internally, the ArgumentNullException.ThrowIfNull() method also throws the ArgumentNullException.

Guidelines
DO verify that non-null reference types parameters are not null and throw an ArgumentNullException when they are.
DO use ArgumentException.ThrowIfNull() to verify values are null in .NET 7.0 or later.
Additional Parameter Validation

There are obviously a myriad of other type constraints that a method may have on its parameters. Perhaps a string argument should not be and empty string, not only comprised of whitespace, or must have “HTTPS” as a prefix. Listing 5.33 displays the full DownloadSSSL() method, demonstrating this validation.

Listing 5.33: Custom Parameter Validation
public class Program
{
    public static int Main(string[] args)
    {
        int result = 0;
        if(args.Length != 2 ) 
        { 
            // Exactly two arguments must be specified; give an error
            Console.WriteLine(
                "ERROR:  You must specify the "
                + "URL and the file name");
            Console.WriteLine(
                "Usage: Downloader.exe <URL> <TargetFileName>");
            result = 1;
        }
        else
        {
            DownloadSSL(args[0], args[1]);
        }
        return result;
    }
 
private static void DownloadSSL(string httpsUrl, string fileName)
{
#if !NET7_0_OR_GREATER
    httpsUrl = httpsUrl?.Trim() ??
        throw new ArgumentNullException(nameof(httpsUrl));
    fileName = fileName ??
        throw new ArgumentNullException(nameof(fileName));
    if (fileName.Trim().Length == 0)
    {
        throw new ArgumentException(
            $"{nameof(fileName)} cannot be empty or only whitespace");
    }
#else
    ArgumentException.ThrowIfNullOrEmpty(httpsUrl = httpsUrl?.Trim()!);
    ArgumentException.ThrowIfNullOrEmpty(fileName = fileName?.Trim()!);
#endif
 
    if (!httpsUrl.ToUpper().StartsWith("HTTPS"))
    {
        throw new ArgumentException("URL must start with 'HTTPS'.");
    }
 
    HttpClient client = new();
    byte[] response =
        client.GetByteArrayAsync(httpsUrl).Result;
    client.Dispose();
    File.WriteAllBytes(fileName!, response);
    Console.WriteLine($"Downloaded '{fileName}' from '{httpsUrl}'.");
}
}

When using .NET 7.0 or higher, you can rely on the ArgumentException.ThrowIfNullOrEmpty() method to check for both null or empty string. And, if you invoke the string.Trim() method when invoking ThrowIfNullOrEmpty(), you can also throw an exception if the argument content is only whitespace. (Admittedly, the exception message will not indicate whitespace only is invalid.)

The equivalent code for .NET 6.0 or earlier is shown in the else directive.

If the null, empty, and whitespace validation pass, Listing 5.33 has an if statement that checks for the “HTTPS” prefix. And, if the validation fails, the resulting code throws and ArgumentException, with a custom message describing the problem.

Introducing the nameof Operator

When the parameter fails validation it is necessary to throw an exception—generally of type ArgumentException() or ArgumentNullException(). Both exceptions take an argument of type string called paramName that identifies the name of the parameter that is invalid. In Listing 5.32, we use leverage the nameof operator12 for this argument. The nameof operator takes an identifier, like the httpsUrl variable, and returns a string representation of that name—in this case, "httpsUrl".

The advantage of using the nameof operator is that if the identifier name changes, then refactoring tools will automatically change the argument to nameof as well. If no refactoring tool is used, the code will no longer compile, forcing the developer to change the argument manually – which is preferable since former value would presumably be invalid. The result is that nameof will even check for spelling errors. The resulting guideline is: DO use nameof for the paramName argument passed into exceptions like ArgumentException and ArgumentNullException that take such a parameter. For more information, see Chapter 18.

Guidelines
DO use nameof(value) (which resolves to “value”) for the paramName argument when creating ArgumentException() or ArgumentNullException() type exceptions. ("value" is the implicit name of the parameter on property setters.)
Avoid Using Exception Handling to Deal with Expected Situations

Developers should avoid throwing exceptions for expected conditions or normal control flow. For example, developers should not expect users to enter valid text when specifying their age.13 Therefore, instead of relying on an exception to validate data entered by the user, developers should provide a means of checking the data before attempting the conversion. (Better yet, they should prevent the user from entering invalid data in the first place.) Exceptions are designed specifically for tracking exceptional, unexpected, and potentially fatal situations. Using them for an unintended purpose such as expected situations will cause your code to be hard to read, understand, and maintain.

Consider, for example, the int.Parse() method we used in Chapter 2 to convert a string to an integer. In this scenario, the code converted user input that was expected to not always be a number. One of the problems with the Parse() method is that the only way to determine whether the conversion will be successful is to attempt the cast and then catch the exception if it doesn’t work. Because throwing an exception is a relatively expensive operation, it is better to attempt the conversion without exception handling. Toward this effort, it is preferable to use one of the TryParse() methods, such as int.TryParse(). It requires the use of the out keyword because the return from the TryParse() function is a bool rather than the converted value. Listing 5.34 is a code snippet that demonstrates the conversion using int.TryParse().

Listing 5.34: Conversion Using int.TryParse()
if (int.TryParse(ageText, out int age))
{
    Console.WriteLine(
        $"Hi { firstName }! " +
        $"You are { age * 12 } months old.");
}
else
{
    Console.WriteLine(
        $"The age entered, { ageText }, is not valid.");
}

With the TryParse() method, it is no longer necessary to include a try/catch block simply for the purpose of handling the string-to-numeric conversion.

Another factor in favor of avoiding exceptions for expected scenarios is performance. Like most languages, C# incurs a slight performance hit when throwing an exception—taking microseconds compared to the nanoseconds most operations take. This delay is generally not noticeable in human time—except when the exception goes unhandled. For example, when Listing 5.25 is executed and the user enters an invalid age, the exception is unhandled and there is a noticeable delay while the runtime searches the environment to see whether there is a debugger to load. Fortunately, slow performance when a program is shutting down isn’t generally a factor to be concerned with.

Guidelines
DO NOT use exceptions for handling normal, expected conditions; use them for exceptional, unexpected conditions.

________________________________________

10. Starting in C# 2.0.
11. Technically it could be caught by a compatible catch filter as well.
12. Introduced in C# 6.0.
13. In general, developers should expect their users to perform unexpected actions; in turn, they should code defensively to handle “stupid user tricks.”
{{ snackbarMessage }}