Chapter 5 discussed using the try/catch/finally blocks for standard exception handling. In that chapter, the catch block always caught exceptions of type System.Exception. This chapter defines some additional details of exception handling—specifically, details surrounding additional exception types, defining custom exceptions, and multiple catch blocks for handling each type. This chapter also details exceptions because of their reliance on inheritance.
Listing 11.1 throws a System.ArgumentException, not the System.Exception type demonstrated in Chapter 5. C# allows code to throw any type that derives (perhaps indirectly) from System.Exception. To throw an exception, you simply prefix the exception instance with the keyword throw. The type of exception used is obviously the type that best describes the circumstances surrounding the error that caused the exception. For example, consider the TextNumberParser.Parse() method in Listing 11.1.
In the call to Array.IndexOf(), we leverage a throw expression1 when the textDigit argument is null.
Instead of throwing System.Exception, it is more appropriate to throw ArgumentException because the type itself indicates what went wrong and includes special parameters for identifying which parameter was at fault.
Two similar exceptions are ArgumentNullException and NullReferenceException. ArgumentNullException should be thrown for the inappropriate passing of null arguments. This is a special case of an invalid argument exception that would more generally (when it isn’t null) be thrown as an ArgumentException or an ArgumentOutOfRangeException.
NullReferenceException is generally an exception that the underlying runtime will throw only with an attempt to dereference a null value—that is, an attempt to call a member on an object whose value is null. Instead of triggering a NullReferenceException to be thrown, programmers should check parameters for null before accessing them and then throw an ArgumentNullException, which can provide more contextual information, such as the parameter name. If there is an innocuous way to proceed even if an argument is null, be sure to use the null-conditional operator2 when dereferencing to avoid the runtime throwing a NullReferenceException.
One important characteristic of the argument exception types (including ArgumentException, ArgumentNullException, and ArgumentOutOfRangeException) is that each has a constructor parameter that allows identification of the argument name as a string.3 The general guideline is to always use the nameof operator for the parameter name of an argument type exception. Chapter 18 provides a full explanation for the nameof operator. Until then, it is sufficient to understand that nameof simply returns the name of the argument identified.
Several other exceptions are intended only for the runtime and derive (sometimes indirectly) from System.SystemException. They include System.StackOverflowException, System.OutOfMemoryException, System.Runtime.InteropServices.COMException, System.ExecutionEngineException, and System.Runtime.InteropServices.SEHException. Do not throw exceptions of these types. Similarly, you should avoid throwing a System.Exception or System.ApplicationException, as these exceptions are so general that they provide little indication of the cause of or resolution to the problem. Instead, throw the most derived exception that fits the scenario. Obviously, developers should avoid creating APIs that could potentially result in a system failure. However, if the executing code reaches a certain state such that continuing to execute is unsafe or unrecoverable, it should call System.Environment.FailFast(). This will immediately terminate the process after potentially writing a message to standard error and, on Microsoft Windows, the Windows Application event log.