What 抯 Wrong with Return Codes? Most programmers have probably written code that looked like this: bool success =CallFunction(); if (!success) { //process the error } This works okay, but every return value has to be checked for an error. If the above was written as CallFunction(); any error return would be thrown away. That抯 where bugs come from. There are many different models for communicating status; some functions may return an HRESULT , some may return a Boolean value, and others may use some other mechanism. In the .Net Runtime world, exceptions are the fundamental method of han-dling error conditions. Exceptions are nicer than return codes because they can抰 be silently ignored.
Trying and Catching To deal with exceptions, code needs to be organized a bit differently. The sections of code that might throw exceptions are placed in a try block, and the code to handle exceptions in the try block is placed in a catch block. Here抯 an example:
using System; class Test { static int Zero =0; public static void Main() { //watch for exceptions here try { int j =22 /Zero; } //exceptions that occur in try are transferred here catch (Exception e) { Console.WriteLine("Exception "+e.Message); } Console.WriteLine("After catch"); } }
The try block encloses an expression that will generate an exception. In this case, it will generate an exception known as DivideByZeroExceptio . When the division takes place, the .Net Runtime stops executing code and searches for a try block surrounding the code in which the exception took place. When it finds a try block, it then looks for associated catch blocks. If it finds catch blocks, it picks the best one (more on how it determines which one is best in a minute), and executes the code within the catch block. The code in the catch block may process the event or rethrow it. The example code catches the exception and writes out the message that is contained within the exception object. The Exception HIErarchy All C# exceptions derive from the class named Exception , which is part of the Common Language Runtime 1 . When an exception occurs, the proper catch block is determined by matching the type of the exception to the name of the exception mentioned. A catch block with an exact match wins out over a more general exception. Returning to the example: using System; class Test { static int Zero =0; public static void Main() { try { int j =22 /Zero; } //catch a specific exception catch (DivideByZeroException e) { Console.WriteLine("DivideByZero {0}",e); } //catch any remaining exceptions catch (Exception e) { Console.WriteLine("Exception {0}",e); } } } The catch block that catches the DivideByZeroException is the more specific match, and is therefore the one that is executed. This example is a bit more complex:
using System; class Test { static int Zero =0; static void AFunction() { int j = 22 / Zero; //the following line is never executed. Console.WriteLine("In AFunction()"); } public static void Main() { try { AFunction(); } catch (DivideByZeroException e) { Console.WriteLine("DivideByZero {0}",e); } } } What happens here? When the division is executed, an exception is generated. The runtime starts searching for a try block in AFunction(), but it doesn抰 find one, so it jumps out of AFunction(), and checks for a try in Main(). It finds one, and then looks for a catch that matches. The catch block then executes. Sometimes, there won抰 be any catch clauses that match. using System; class Test { static int Zero =0; static void AFunction() { try { int j =22 /Zero; } //this exception doesn't match catch (ArgumentOutOfRangeException e) { Console.WriteLine("OutOfRangeException:{0}",e); } Console.WriteLine("In AFunction()"); } public static void Main() { try { AFunction(); } //this exception doesn't match catch (ArgumentException e) { Console.WriteLine("ArgumentException {0}",e); } } } Neither the catch block in AFunction()nor the catch block in Main()matches the exception that抯 thrown. When this happens, the exception is caught by the "last chance" exception handler. The action taken by this handler depends on how the runtime is configured, but it will usually bring up a dialog box containing the excep-tion information and halt the program. Passing Exceptions on to the Caller It抯 sometimes the case that there抯 not much that can be done when an exception occurs; it really has to be handled by the calling function. There are three basic ways to deal with this, which are named based on their result in the caller: Caller Beware, Caller Confuse, and Caller Inform. Caller Beware The first way is to merely not catch the exception. This is sometimes the right design decision, but it could leave the object in an incorrect state, causing problems when the caller tries to use it later. It may also give insufficIEnt information to the caller. Caller Confuse The second way is to catch the exception, do some cleanup, and then rethrow the exception: using System; public class Summer { int sum =0; int count =0; float average; public void DoAverage() { try { average =sum /count; } catch (DivideByZeroException e) { //do some cleanup here throw e; } } } class Test { public static void Main() { Summer summer =new Summer(); try { summer.DoAverage(); } catch (Exception e) { Console.WriteLine("Exception {0}",e); } } } This is usually the minimal bar for handling exceptions; an object should always maintain a valid state after an exception. This is called Caller Confuse because while the object is in a valid state after the exception occurs, the caller often has little information to go on. In this case, the exception information says that a DivideByZeroException occurred somewhere in the called function, without giving any insight into the details of the exception or how it might be fixed. Sometimes this is okay if the exception passes back obvious information. Caller Inform In Caller Inform, additional information is returned for the user. The caught exception is wrapped in an exception that has additional information. using System; public class Summer { int sum =0; int count =0; float average; public void DoAverage() { try { average =sum /count; } catch (DivideByZeroException e) { //wrap exception in another one, //adding additional context. throw (new DivideByZeroException( "Count is zero in DoAverage()",e)); } } } public class Test { public static void Main() { Summer summer =new Summer(); try { summer.DoAverage(); } catch (Exception e) { Console.WriteLine("Exception:{0}",e); } } } When the DivideByZeroException is caught in the DoAverage()function, it is wrapped in a new exception that gives the user additional information about what caused the exception. Usually the wrapper exception is the same type as the caught exception, but this might change depending on the model presented to the caller. This program generates the following output: Exception:System.DivideByZeroException:Count is zero in DoAverage()---> System.DivideByZeroException at Summer.DoAverage() at Summer.DoAverage() at Test.Main() Ideally, each function that wants to rethrow the exception will wrap it in an excep-tion with additional contextual information. User-Defined Exception Classes One drawback of the last example is that the caller can抰 tell what exception hap-pened in the call to DoAverage()by looking at the type of the exception. To know that the exception was because the count was zero, the expression message would have to be searched for the string is zero ". That would be pretty bad, since the user wouldn抰 be able to trust that the text would remain the same in later versions of the class, and the class writer wouldn抰 be able to change the text. In this case, a new exception class can be created. using System; public class CountIsZeroException:Exceptio { public CountIsZeroException() { } public CountIsZeroException(string message) :base(message) { } public CountIsZeroException(string message,Exception inner) :base(message,inner) { } } public class Summer { int sum =0; int count =0; float average; public void DoAverage() { if (count ==0) throw(new CountIsZeroException("Zero count in DoAverage")); else average =sum /count; } } class Test { public static void Main() { Summer summer =new Summer(); try { summer.DoAverage(); } catch (CountIsZeroException e) { Console.WriteLine("CountIsZeroException:{0}",e); } } }
DoAverage()now determines whether there would be an exception (whether count is zero), and if so, creates a CountIsZeroException and throws it. Finally Sometimes, when writing a function, there will be some cleanup that needs to be done before the function completes, such as closing a file. If an exception occurs, the cleanup could be skipped: using System; using System.IO; class Processor { int count; int sum; public int average; void CalculateAverage(int countAdd,int sumAdd) { count +=countAdd; sum +=sumAdd; average =sum /count; } public void ProcessFile() { FileStream f =new FileStream("data.txt",FileMode.Open); try { StreamReader t =new StreamReader(f); string line; while ((line =t.ReadLine())!=null) { int count; int sum; count =Int32.FromString(line); line =t.ReadLine(); sum =Int32.FromString(line); CalculateAverage(count,sum); } f.Close(); } //always executed before function exit,even if an //exception was thrown in the try. finally { f.Close(); } } } class Test { public static void Main() { Processor processor =new Processor(); try { processor.ProcessFile(); } catch (Exception e) { Console.WriteLine("Exception:{0}",e); } } }
This example walks through a file, reading a count and sum from a file and using it to accumulate an average. What happens, however, if the first count read from the file is a zero? If this happens, the division in CalculateAverage()will throw a DivideByZero- Exception , which will interrupt the file-reading loop. If the programmer had written the function without thinking about exceptions, the call to file.Close() would have been skipped, and the file would have remained open. The code inside the finally block is guaranteed to execute before the exit of the function, whether there is an exception or not. By placing the file.Close()call in the finally block, the file will always be closed. EfficIEncy and Overhead In languages without garbage collection, adding exception handling is expensive, since all objects within a function must be tracked to make sure that they are properly destroyed if an exception is thrown. The required tracking code both adds execution time and code size to a function. In C#, however, objects are tracked by the garbage collector rather than the compiler, so exception handling is very inexpensive to implement and imposes little runtime overhead on the program when the exceptional case doesn抰 occur. Design Guidelines Exceptions should be used to communicate exceptional conditions. Don抰 use them to communicate events that are expected, such as reaching the end of a file. In the normal Operation of a class, there should be no exceptions thrown. Conversely, don抰 use return values to communicate information that would be better contained in an exception. If there抯 a good predefined exception in the System namespace that describes the exception condition梠ne that will make sense to the users of the class梪se that one rather than defining a new exception class, and put specific information in the message. If the user might want to differentiate one case from others where that same exception might occur, then that would be a good place for a new excep-tion class. Finally, if code catches an exception that it isn抰 going to handle, consider whether it should wrap that exception with additional information before rethrowing it.