11. Exception Handling

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.

There are four categories included in exception handling: multiple exception types, catching experiences, general catch block, and guidelines.

Multiple Exception Types

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.

Begin 6.0
Begin 7.0

Listing 11.1: Throwing an Exception

public sealed class TextNumberParser
{
  public static int Parse(string textDigit)
  {
      string[] digitTexts =
          { "zero", "one", "two", "three", "four",
              "five", "six", "seven", "eight", "nine" };

      int result = Array.IndexOf(
          digitTexts,
            // Leveraging C# 2.0's null-coalescing operator
          (textDigit??
            // Leveraging C# 7.0's throw expression
            throw new ArgumentNullException(nameof(textDigit))
          ).ToLower());
      if (result < 0)
      {

          // Leveraging C# 6.0's nameof operator
          throw new ArgumentException(                              
              "The argument did not represent a digit",             
              nameof(textDigit));                                   
      }

      return result;
  }
}
End 7.0

In the call to Array.IndexOf(), we leverage a C# 7.0 throw expression when the textDigit argument is null. Prior to C# 7.0, throw expressions were not allowed; only throw statements were allowed. As a result, two separate statements were required: one checking for null and the other to throw the exception. You could not embed the throw within the same statement as a null-coalescing operator, for example.

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 parameter 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 C# 6.0 null-conditional operator when dereferencing to avoid the runtime throwing a NullReferenceException.

6.0

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. Prior to C# 6.0, this meant hardcoding a magic string (e.g., “textDigit”) to identify the parameter name. The problem with this approach is that if the parameter name ever changed, developers had to remember to update the magic string. Fortunately, C# 6.0 and later provide a nameof operator, which takes the parameter name identifier and generates the parameter name string at compile time (see nameof(textDigit) in Listing 11.1). The advantage of this approach is that now the IDE can use refactoring tools (such as automatic renaming) to change the identifier everywhere, including when it is used as an argument to the nameof operator. Additionally, if the parameter name changes (without the use of a refactoring tool), the compiler will generate an error if the identifier passed to the nameof operator no longer exists. Moving forward, with C# 6.0 and later, 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.

End 6.0

Catching Exceptions

Throwing a particular exception type enables the catcher to use the exception’s type itself to identify the problem. It is not necessary, in other words, to catch the exception and use a switch statement on the exception message to determine which action to take in light of the exception. Instead, C# allows for multiple catch blocks, each targeting a specific exception type, as Listing 11.2 shows.

Listing 11.2: Catching Different Exception Types

using System;

public sealed class Program
{
  public static void Main(string[] args)
  {
      try
      {

              // ...
              throw new InvalidOperationException(
                  "Arbitrary exception");
              // ...
      }
      catch(Win32Exception exception)
          when(exception.NativeErrorCode == 42)
      {
          // Handle Win32Exception where
          // ErrorCode is 42
      }
      catch (ArgumentException exception)
      {
          // Handle ArgumentException
      }
      catch (InvalidOperationException exception)
      {
         bool exceptionHandled=false;
          // Handle InvalidOperationException
         // ...
          if(!exceptionHandled)
          {
               throw;
          }
       }
      catch (Exception exception)
      {
          // Handle Exception
      }
      finally
      {
          // Handle any cleanup code here as it runs
          // regardless of whether there is an exception
      }
  }
}

Listing 11.2 includes five catch blocks, each handling a different type of exception. When an exception occurs, the execution will jump to the catch block with the exception type that most closely matches the exception. The closeness of a match is determined by the inheritance chain. For example, even though the exception thrown is of type System.Exception, this “is a” relationship occurs through inheritance because System.InvalidOperationException ultimately derives from System.Exception. Since the exception type InvalidOperationException most closely matches the exception thrown, the catch(InvalidOperationException...) block will catch the exception and not the catch(Exception...) block.

Begin 6.0

Starting with C# 6.0, an additional conditional expression is available for catch blocks. Instead of limiting whether a catch block matches based only on an exception type match, there is also a conditional clause. This when clause allows you to supply a Boolean expression; the catch block handles the exception only if the condition is true. In Listing 11.2, this is an equality comparison operator. For more complex logic, you could make a method call to check for a condition.

Of course, you could also simply place the conditional check as an if block within the catch body. However, doing so causes the catch block to become the handler for the exception before the condition is checked. It is difficult to write code that allows a different catch block to handle the exception in the scenario where the condition is not met. However, with the exception condition, it is now possible to examine the program state (including the exception) without having to catch and rethrow the exception.

Use conditional clauses with caution: If the conditional expression itself throws an exception, then that new exception will be ignored and the condition will be treated as false. For this reason, you should avoid throwing exceptions for the exception conditional expression.

End 6.0

Catch blocks must appear in order, from most specific to most general, to avoid a compile-time error. For example, moving the catch(Exception...) block before any of the other exceptions will result in a compile error, since all prior exceptions derive from System.Exception at some point in their inheritance chain.

As shown with the catch (SystemException){ }) block, a named parameter for the catch block is not required. In fact, a final catch without even the type parameter is allowable, as you will see in the next section.

Rethrowing an Existing Exception

In the InvalidOperationException catch block, a throw statement appears without any identification of the exception to throw (throw is on its own), even though an exception instance (exception) appears in the catch block scope that could be rethrown. Throwing a specific exception would update all the stack information to match the new throw location. As a result, all the stack information indicating the call site where the exception originally occurred would be lost, making it significantly more difficult to diagnose the problem. For this reason, C# supports a throw statement or expression (C# 7.0 or later) without the explicit exception reference as long as it occurs within a catch block. This way, code can examine the exception to determine if it is possible to fully handle it and, if not, rethrow the exception (even though not specified explicitly) as though it was never caught and without replacing any stack information.

Begin 5.0

With the ExceptionDispatchInfo.Throw() method, the compiler doesn’t treat the code as a return statement in the same way that it might a normal throw statement. For example, if the method signature returned a value but no value was returned from the code path with ExceptionDispatchInfo.Throw(), the compiler would issue an error indicating no value was returned. On occasion, therefore, developers may be forced to follow ExceptionDispatchInfo.Throw() with a return statement even though such a statement would never execute at runtime—the exception would be thrown instead.

End 5.0

General Catch Block

Begin 2.0

C# requires that any object that code throws must derive from System.Exception. However, this requirement is not universal to all languages. C++, for example, allows any object type to be thrown, including managed exceptions that don’t derive from System.Exception. All exceptions,1 whether derived from System.Exception or not, will propagate into C# assemblies as derived from System.Exception. The result is that System.Exception catch blocks will catch all exceptions not caught by earlier blocks.

1. Starting with C# 2.0.

C# also supports a general catch block (catch{ }) that behaves identically to the catch(System.Exception exception) block, except that there is no type or variable name. Also, the general catch block must appear last within the list of catch blocks. Since the general catch block is identical to the catch(System.Exception exception) block and the general catch block must appear last, the compiler issues a warning if both exist within the same try/catch statement because the general catch block will never be invoked.

End 2.0

Guidelines for Exception Handling

Exception handling provides much-needed structure to the error-handling mechanisms that preceded it. However, it can still lead to some unwieldy results if used haphazardly. The following guidelines offer some best practices for exception handling.

  • Catch only the exceptions that you can handle.

    It is generally possible to handle some types of exceptions but not others. For example, opening a file for exclusive read-write access may throw a System.IO.IOException because the file is already in use. In catching this type of exception, the code can report to the user that the file is in use and allow the user the option of canceling the operation or retrying it. Only exceptions for which there is a known action should be caught. Other exception types should be left for callers higher in the stack.

  • Don’t hide (bury) exceptions you don’t fully handle.

    New programmers are often tempted to catch all exceptions and then continue executing instead of reporting an unhandled exception to the user. However, this practice may result in a critical system problem going undetected. Unless code takes explicit action to handle an exception or explicitly determines certain exceptions to be innocuous, catch blocks should rethrow exceptions instead of catching them and hiding them from the caller. In most cases, catch(System.Exception) and general catch blocks should occur higher in the call stack unless the block ends by rethrowing the exception.

  • Use System.Exception and general catch blocks rarely.

    Begin 4.0

    Almost all exceptions derive from System.Exception. However, the best way to handle some System.Exceptions is to allow them to go unhandled or to gracefully shut down the application sooner rather than later. These exceptions include cases such as System.OutOfMemoryException and System.StackOverflowException. Fortunately,2 such exceptions default to a nonrecoverable state, such that catching them without rethrowing them would cause the CLR to rethrow them anyway. These exceptions are runtime exceptions that the developer cannot write code to recover from. Therefore, the best course of action is to shut down the application—something the runtime forces.

    2. Starting with Common Language Runtime (CLR) 4. Prior to CLR 4, code should catch such exceptions only to run cleanup or emergency code (such as saving any volatile data) before shutting down the application or rethrowing the exception with throw;.

    End 4.0
  • Avoid exception reporting or logging lower in the call stack.

    Often, programmers are tempted to log exceptions or report exceptions to the user at the soonest possible location in the call stack. However, these locations are seldom able to handle the exception fully; instead, they resort to rethrowing the exception. Such catch blocks should not log the exception or report it to a user while in the bowels of the call stack. If the exception is logged and rethrown, the callers higher in the call stack may do the same, resulting in duplicate log entries of the exception. Worse, displaying the exception to the user may not be appropriate for the type of application. (Using System.Console.WriteLine() in a Windows application will never be seen by the user, for example, and displaying a dialog in an unattended command-line process may go unnoticed and freeze the application.) Logging- and exception-related user interfaces should be reserved for use higher up in the call stack.

  • Use throw; rather than throw <exception object> inside a catch block.

    It is possible to rethrow an exception inside a catch block. For example, the implementation of catch(ArgumentNullException exception) could include a call to throw exception. However, rethrowing the exception like this will reset the stack trace to the location of the rethrown call instead of reusing the original throw point location. Therefore, unless you are rethrowing with a different exception type or intentionally hiding the original call stack, use throw; to allow the same exception to propagate up the call stack.

  • Favor exception conditions to avoid rethrowing an exception inside a catch block.

    Begin 6.0

    On occasions when you find yourself catching an exception that you can’t actually handle appropriately and therefore need to rethrow, favor using an exception condition to avoid catching the exception in the first place.

  • Avoid throwing exceptions from exception filters.

    When providing an exception filter, avoid code that throws an exception. Throwing an exception from an exception filter will result in a false condition, and the exception occurrence will be ignored. For this reason, you should consider placing complicated conditional checks into a separate method that is wrapped in a try/catch block that handles the exception explicitly.

  • Avoid exception conditionals that might change over time.

    If an exception filter evaluates conditions such as exception messages that could potentially change with localization or changed message, the expected exception condition will not get caught, unexpectedly changing the business logic. For this reason, you should ensure exception conditions are valid over time.

    End 6.0
  • Use caution when rethrowing different exceptions.

    From inside a catch block, rethrowing a different exception will not only reset the call stack but also hide the original exception. To preserve the original exception, set the new exception’s InnerException property, generally assignable via the constructor. Rethrowing a different exception should be reserved for the following situations:

    1. Changing the exception type clarifies the problem.

      For example, in a call to Logon(User user), rethrowing a different exception type is perhaps more appropriate than propagating System.IO.IOException when the file with the user list is inaccessible.

    2. Private data is part of the original exception.

      In the preceding scenario, if the file path is included in the original System.IO.IOException, thereby exposing private security information about the system, the exception should be wrapped. This assumes, of course, that InnerException is not set with the original exception. (Funnily enough, a very early version of CLR version 1 [pre-alpha, even] had an exception that said something like “Security exception: You do not have permission to determine the path of c: empfoo.txt”.)

    3. The exception type is too specific for the caller to handle appropriately.

      For example, instead of throwing an exception specific to a particular database system, use a more generic exception so that database-specific code higher in the call stack can be avoided.

Defining Custom Exceptions

Once throwing an exception becomes the best course of action, it is preferable to use framework exceptions because they are well established and understood. Instead of throwing a custom invalid argument exception, for example, it is preferable to use the System.ArgumentException type. However, if the developers using a particular API will take special action—the exception-handling logic will vary to handle a custom exception type, for instance—it is appropriate to define a custom exception. For example, if a mapping API receives an address for which the ZIP code is invalid, instead of throwing System.ArgumentException, it may be better to throw a custom InvalidAddressException. The key is whether the caller is likely to write a specific InvalidAddressException catch block with special handling rather than just a generic System.ArgumentException catch block.

Defining a custom exception simply involves deriving from System.Exception or some other exception type. Listing 11.4 provides an example.

Listing 11.4: Creating a Custom Exception

class DatabaseException : System.Exception
{
  public DatabaseException(
      string? message,
      System.Data.SqlClient.SQLException? exception)
      : base(message, innerException: exception)
  {
      // ...
  }
  public DatabaseException(
      string? message,
      System.Data.OracleClient.OracleException? exception)
      : base(message, innerException: exception)
  {
      // ...
  }
  public DatabaseException()
  {
      // ...
  }
  public DatabaseException(string message)
  {
      // ...
  }

  public DatabaseException(
      string? message, Exception? exception)
      : base(message, innerException: exception)
  {
      // ...
  }
}

This custom exception might be created to wrap proprietary database exceptions. Since Oracle and SQL Server (for example) throw different exceptions for similar errors, an application could define a custom exception that standardizes the database-specific exceptions into a common exception wrapper that the application can handle in a standard manner. That way, whether the application was using an Oracle or a SQL Server back-end database, the same catch block could be used to handle the error higher up the stack.

The only requirement for a custom exception is that it derives from System.Exception or one of its descendants. However, other good practices for custom exceptions are as follows:

  • All exceptions should use the “Exception” suffix. This way, their purpose is easily established from their name.

  • Generally, all exceptions should include constructors that take no parameters, a string parameter, and a parameter set consisting of a string and an inner exception. Furthermore, since exceptions are usually constructed within the same statement in which they are thrown, any additional exception data should be allowed as part of the constructor. (The obvious exception to creating all these constructors is if certain data is required and a constructor circumvents the requirements.)

  • The inheritance chain should be kept relatively shallow (with fewer than approximately five levels).

The inner exception serves an important purpose when rethrowing an exception that is different from the one that was caught. For example, if a System.Data.SqlClient.SqlException is thrown by a database call but is caught within the data access layer and will be rethrown as a DatabaseException, the DatabaseException constructor that takes the SqlException (or inner exception) will save the original SqlException in the InnerException property. That way, if they require additional details about the original exception, developers can retrieve the exception from the InnerException property (e.g., exception.InnerException).

Rethrowing a Wrapped Exception

On occasion, an exception thrown at a lower level in the stack will no longer make sense when caught at a higher level. For example, consider a System.IO.IOException that occurs because a system is out of disk space on the server. A client catching such an exception would not necessarily be able to understand the context of why there was even I/O activity. Similarly, consider a geographic coordinate request API that throws a System.UnauthorizedAccessException (an exception totally unrelated to the API called). In this second example, the caller has no context for understanding what the API call has to do with security. From the perspective of the code that invokes the API, these exceptions cause more confusion than they help diagnose. Instead of exposing such exceptions to the client, it might make sense to first catch the exception and then throw a different exception, such as InvalidOperationException (or even perhaps a custom exception), as a means of communicating that the system is in an invalid state. In such scenarios, be sure to set the InnerException property of the wrapping exception (generally via a constructor call such as new InvalidOperationException(string, Exception)) so that there is additional context that can be used for diagnostic purposes by someone closer to the framework that was invoked.

An important detail to remember when considering whether to wrap and rethrow an exception is the fact that the original stack trace—which provides the context of where the exception was thrown—will be replaced with the new stack trace of where the wrapping exception is thrown (assuming ExceptionDispatchInfo is not used). Fortunately, when the original exception is embedded into the wrapping exception, the original stack trace is still available.

Ultimately, the intended recipient of the exception is the programmer writing code that calls your API—possibly incorrectly. Therefore, you should provide as much information to her that indicates both what the programmer did wrong and—perhaps more important—how to fix it. The exception type is a critical piece of the communication mechanism, so you must choose it wisely.

Summary

Throwing an exception produces a significant performance hit. A single exception causes lots of runtime stack information to be loaded and processed—data that would not otherwise be loaded—and it takes a considerable amount of time to handle. As pointed out in Chapter 5, you should use exceptions only to handle exceptional circumstances; APIs should provide mechanisms to check whether an exception will be thrown instead of forcing a particular API to be called to determine whether an exception will be thrown.

The next chapter introduces generics—a C# 2.0 feature that significantly enhances code written in C# 1.0. In fact, it essentially deprecates any use of the System.Collections namespace, which was formerly used in nearly every project.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.163.58