- Reference Types
- Object Construction/Destruction
- Object-Oriented Features
- Exception Handling
- In Brief
Exception Handling
Exceptions are C# objects that are used to handle unexpected errors in an application. The exception describes the error with messages, error codes, and stack traces. Even though exceptions occur at unexpected times, they always behave in a predictable manner. Because of the predictable behavior of a raised exception, it can be handled with exception handlers. The ability to plan for and handle exceptions is an essential ingredient in building reliable enterprise-class applications. The following sections explain how to find out whether an exception can be raised, handle raised exceptions, and create custom exceptions to meet all error-handling needs of an application.
Handling Exceptions with try/catch/finally Blocks
Before moving into exception-handling strategies and other important information, it is necessary to fully understand the mechanics and syntax of exception handling. Exceptions are handled by using try/catch/finally blocks. Each of these keywords decorates its own block to handle exceptions. A single try block encloses code that could possibly generate an exception. Following the try block are one or more catch blocks that are ordered from most specific exception to more general. The catch blocks are where raised exceptions are handled. A single finally block can follow the catch blocks and hold code that is guaranteed to be executed regardless of whether an exception is raised or not. A finally block is the appropriate place to put code that releases resources like database connections and file streams. Listing 3.11 implements a try/catch/finally block.
Listing 3.11 Demonstration of a try/catch/finally Block (TryCatchFinally.cs)
using System; using System.IO; class TryCatchFinally { static void Main() { Stream stream = null; try { stream = new FileStream( "MyFile.txt", FileMode.OpenOrCreate, FileAccess.Read); stream.Write(new byte[] {1, 2, 3}, 0, 3); } catch (IOException ioe) { Console.WriteLine("Caught IOException: \n\n" + ioe.ToString()); } catch (Exception ex) { Console.WriteLine("Caught Exception: \n\n" + ex.ToString()); } finally { Console.WriteLine("\nExecuting Finally Block."); if (stream != null) stream.Close(); } Console.ReadLine(); } }
The code in Listing 3.11 generates a System.NotSupportedException exception. The exception was generated because of a write attempt to a read-only stream. Although the IOException catch block didn't (and shouldn't) catch it, the Exception catch block did.
Because this is a stream operation, it would have been logical to assume that an IOException exception would have been generated for any problems, but this is not the case. This illustrates that you can't make any assumptions about the hierarchy of a particular exception. To be truly sure of what exceptions a given method will execute, you should look in the documentation. If you don't have documentation, and the source code is unavailable, and you have an assembly that hasn't been obfuscated, another option is to use the Borland Reflection tool, discussed in Chapter 1, to view the contents of an assembly to find out what exceptions a type will raise.
Some mixing and matching takes place, depending on the type of handling needed. For example, a try/catch block without a finally block may be implemented when there is no need to provide guaranteed code execution. Another possible scenario is that an algorithm doesn't want to catch the exception, but needs to guarantee that certain code will be executed, which would call for a try/finally block without any catch blocks.
Creating Custom Exception Types
When designing custom types, it may be necessary to throw exceptions. If an existing Exception derived type handles the condition, use the existing exception type. If you want to find out what exceptions are available in .NET, see the .NET framework help documentation. The System.Exception type has a link for derived classes on its overview page. However, there are times when existing exception types are not descriptive enough. In those cases, it is better to define a custom exception.
Custom exceptions can be derived from either the System.Exception class or another more specific exception class in the exception hierarchy. There are no set rules for defining which exception to derive from, but it should be one related to the condition the custom exception is trying to resolve. If nothing related exists or fits right, it is perfectly okay to derive from System.Exception. Listing 3.12 shows a custom exception that derives from System.Exception.
Listing 3.12 Building a Custom Exception Type (CustomException.cs)
using System; using System.IO; public class CustomException : Exception { object extraInfo; public CustomException(string message, object extra) : base(message) { extraInfo = extra; } public override string ToString() { return "CustomException: " + extraInfo.ToString() + "\n" + base.Message + "\n" + base.StackTrace; } } class CustomExceptionGenerator { static void Main() { try { SomeMethod(); } catch (CustomException ce) { Console.WriteLine(ce.ToString()); } Console.ReadLine(); } public static void SomeMethod() { //something went wrong throw new CustomException("Something went wrong", "ExtraInfo"); } }
The CustomException class in Listing 3.12 inherits System.Exception and defines its own member named extraInfo. When declaring the CustomException constructor, both a message and a value to initialize extraInfo are accepted. The message parameter is used to properly initialize the base class through the syntax: : base(message). CustomException overrides the ToString method to provide a custom message. A typical custom exception would have implemented a property for extraInfo also to provide access to this information to catch blocks handling this exception. Exceptions are raised with the throw statement, as shown in SomeMethod of Listing 3.12.