10.2 Handling Multiple Types of Exceptions

 <  Day Day Up  >  

Our first exception example was overly simplistic. What happens if our method generates more than one kind of error? Are they all sent to the same catch block? Well, that's up to the developer; they certainly could be, but it's more typical and better practice for different kinds of errors to be handled by separate catch blocks. Let's examine why.

Suppose we wanted a finer-grained set of error messages in our setWidth( ) method: one for general invalid data, one for too small a width, and one for too large a width. Our revised setWidth( ) method might look like this:

 public function setWidth (w:Number):Void {   if (isNaN(w)  w == null) {     throw new Error("Illegal Box dimension specified.");   } else if (w <= 0) {     throw new Error("Box dimensions must be greater than 0.");   } else if (w > Number.MAX_VALUE) {     throw new Error("Box dimensions must be less than Number.MAX_VALUE.");   }   width = w; } 

To handle the three possible error messages in our new setWidth( ) message, we might be tempted to code our try/catch/finally statement as follows :

 try {   b.setWidth(someWidth);   // If we get this far, no exception occurred; proceed as planned.   trace("Width set successfully."); } catch (e:Error) {   switch (e.message) {     case "Illegal Box dimension specified.":     trace("An error occurred: " + e.message);     trace("Please specify a valid dimension.");     break;     case "Box dimensions must be greater than 0.":     trace("An error occurred: " + e.message);     trace("Please specify a larger value.");     break;     case "Box dimensions must be less than Number.MAX_VALUE.":     trace("An error occurred: " + e.message);     trace("Please specify a smaller value.");     break;   } } 

Admittedly, that code does work, but it's fraught with problems. First, and most serious, the errors are distinguished from one another only by the text in a string that is hidden within the Box class. Each time we want to check the type of an error, we have to look inside the Box class and find the message string. Using the string for error identification across multiple methods and classes is highly prone to human error and makes our code difficult to maintain. Second, the switch statement itself is barely more readable than our original, if/else statement (the one we used before we added exceptions to the setWidth( ) method). We're not much farther ahead than we would be if we had used, say, numeric error codes instead of formal exceptions.

Fortunately, there's a formal (and elegant) way to handle multiple exception types. Each try block can have any number of supporting catch blocks. When an exception is thrown in a try block that has multiple catch blocks, the interpreter executes the catch block whose parameter's datatype matches the datatype of the value originally thrown. The datatypes of the catch parameter and thrown value are considered a match if they are identical or if the catch parameter type is a superclass of the thrown value's type.

Here's the general syntax of a try statement with multiple catch blocks:

 try {   // Code that might generate an exception. } catch (e:   ErrorType1   ) {   // Error-handling code for   ErrorType1   . } catch (e:   ErrorType2   ) {   // Error-handling code for   ErrorType2   . } catch (e:   ErrorTypen   ) {   // Error-handling code for   ErrorTypen   . } 

If a throw statement in the preceding try block were to throw an expression of type ErrorType1 , then the first catch block would be executed. For example, the following code causes the first catch block to execute:

 throw new ErrorType1( ); 

If a throw statement were to pass an expression of type ErrorType2 , then the second catch clause would be executed, and so on. As we learned earlier, in ActionScript the throw statement expression can belong to any datatype. However, as an OOP best practice, we should throw only instances of the Error class or one of its subclasses (this best practice follows Java, where throw can be used only with Throwable and its subclasses).

If we want to throw multiple kinds of exceptions in an application, we should define an Error subclass for each kind of exception. It is up to you as the developer to decide what level of granularity you require (i.e., to what degree you need to differentiate among different error conditions). However, don't confuse the following discussion of how to implement granularity in error handling as an insistence that you must implement such granularity.

10.2.1 Determining Exception Type Granularity

Should you define an Error subclass for each error condition? Typically, no, you won't need that level of granularity because in many cases multiple error conditions can be treated in the same way. If you don't need to differentiate among multiple error conditions, you can group them together under a single custom Error subclass. For example, you might define a single Error subclass named InvalidInputException to handle a wide range of input problems.

That said, you should define a separate Error subclass for each error condition that you want to distinguish from other possible conditions. To help you understand when you should create a new subclass for a given error condition and to demonstrate how to group multiple conditions into a single subclass, let's return to our familiar Box class.

Earlier we generated three exceptions from the Box.setWidth( ) method: one for general invalid data, one for too small a width, and one for too large a width. All three Box - related exceptions used the generic Error class. Here's the code again:

 public function setWidth (w:Number):Void {   if (isNaN(w)  w == null) {     throw new Error("Illegal Box dimension specified.");   } else if (w <= 0) {     throw new Error("Box dimensions must be greater than 0.");   } else if (w > Number.MAX_VALUE) {     throw new Error("Box dimensions must be less than Number.MAX_VALUE.");   }   width = w; } 

In the preceding code, to differentiate Box exceptions from all other exceptions in our application, we use the Error class's message property, which, as we just learned, made our exceptions awkward to use and prone to human error. A better way to set Box -related data errors apart from other errors in our application is to define a custom Error subclass, BoxDimensionException , as follows:

   // Code in BoxDimensionException.as:   class BoxDimensionException extends Error {   public var message:String = "Illegal Box dimension specified."; } 

With our BoxDimensionException class in place, our Box.setWidth( ) method can throw its very own type of error, as follows:

 public function setWidth (w:Number):Void {   if (isNaN(w)  w == null) {  // Throw a   BoxDimensionException   instead of an   Error    .  throw new BoxDimensionException( );  } else if (w <= 0) {  throw new BoxDimensionException( );  } else if (w > Number.MAX_VALUE) {  throw new BoxDimensionException( );  }   width = w; } 

Notice that the preceding method definition throws the same error type ( BoxDimensionException ) for all three Box -related error conditions. As developers of the Box class, we now face the crux of the error granularity issue. We must decide not only how distinguishable we want Box error messages to be from other application errors, but also how distinguishable we want those errors to be from one another. We have the following options:


Option 1: use a single Box error class.

Leave the preceding setWidth( ) method definition as-is. As we'll see shortly, this option lets us distinguish Box errors from other generic errors in the program, but it does not help us distinguish internally among the three varieties of Box -related errors (invalid data, too small a width, and too large a width).


Option 2: simplify code, but still use a single Box error class.

Refactor the setWidth( ) method to check for all three error conditions using a single if statement. This option is the same as the previous option, but uses cleaner code.


Option 3: use debugging messages to distinguish among errors.

Add configurable debugging messages to the BoxDimensionException class. This option adds slightly more granularity than the previous two options but only for the sake of the developer and only during debugging.


Option 4: create a custom exception class for each error condition.

Create two custom BoxDimensionException subclasses, BoxUnderZeroException and BoxOverflowException . This option provides the most granularity ”it lets a program respond independently to the three varieties of Box -related errors using formal branching logic.

Let's consider the preceding options in turn .

10.2.1.1 Using a single custom exception type

Our first option is to accept the preceding setWidth( ) definition, which throws the same error type ( BoxDimensionException ) for all three Box -related error conditions. Because the method uses BoxDimensionException and not Error to throw exceptions, Box exceptions are already distinguishable from other generic exceptions. Users of the setWidth( ) method can use code such as the following to discriminate between Box -related errors and other generic errors:

 var b:Box = new Box( ); var someWidth:Number = -10; try {   // This call to   setWidth( )   will generate a   BoxDimensionException   .   b.setWidth(someWidth);   // Other statements in this   try   block might generate other generic errors.   // For demonstration purposes, we'll throw a generic error directly.   throw new Error("A generic error."); } catch (e:BoxDimensionException) {   // Handle   Box   dimension errors here.   trace("An error occurred: " + e.message);   trace("Please specify a valid dimension."); } catch (e:Error) {   // Handle all other errors here.   trace("An error occurred: " + e.message); } 

For many applications, the level of error granularity provided by BoxDimensionException is enough. In such a case, we should at least refactor the setWidth( ) method so that it doesn't contain redundant code (throwing the BoxDimensionException three times). Here's the refactored code (which was option 2 in our earlier list):

 public function setWidth (w:Number):Void {   if ((isNaN(w)  w == null)  (w <= 0)  (w > Number.MAX_VALUE)) {     throw new BoxDimensionException( );   }   width = w; } 

10.2.1.2 Using configurable debugging messages

Now let's turn to option 3 (adding configurable debugging messages to the BoxDimensionException class). Options 1 and 2 let us distinguish a Box exception from other exceptions in the application but didn't help us distinguish, say, an overflow exception from a less-than -zero exception. If we feel that it's difficult to debug a Box dimension problem without knowing whether a box is too big or too small, we can adjust the BoxDimensionException class so that it accepts an optional description (the equivalent of a proverbial "note to self"). Here's the adjusted BoxDimensionException class:

 class BoxDimensionException extends Error {   // The default error message still stands.   public var message:String = "Illegal Box dimension specified.";   // Provide a constructor that allows a custom message to be supplied.   public function BoxDimensionException (boxErrMsg:String) {     super(boxErrMsg);   } } 

To make use of our adjusted BoxDimensionException class in setWidth( ) , we revert to our setWidth( ) code used in option 1 and add debugging error messages, as follows:

 public function setWidth (w:Number):Void {   if (isNaN(w)  w == null) {     // The default error message is fine in this case,      // so don't bother specifying a custom error message.     throw new BoxDimensionException( );   } else if (w <= 0) {     // Here's the custom "too small" error message.     throw new BoxDimensionException("Box dimensions must "                                      + "be greater than 0.");   } else if (w > Number.MAX_VALUE) {     // Here's the custom "too big" error message.     throw new BoxDimensionException("Box dimensions must be less "                                     + "than Number.MAX_VALUE.");   }   width = w; } 

Now that setWidth( ) supplies custom error messages, we'll have an easier time debugging a Box problem because we'll know more information when the error occurs. Our use of the setWidth( ) method has not changed, but we're better informed when something goes wrong, as shown next :

 var b:Box = new Box( ); var someWidth:Number = -10; try {   b.setWidth(someWidth); } catch (e:BoxDimensionException) {   // Handle   Box   dimension errors here.   // In this case, the helpful debugging output is:   //   An error occurred: Box dimensions must be greater than 0.   trace("An error occurred: " + e.message); } catch (e:Error) {   // Handle all other errors here.   trace("An error occurred: " + e.message); } 

10.2.1.3 Multiple custom BoxDimensionException subclasses

Option 3 (adding configurable debugging messages to the BoxDimensionException class) helped us investigate a problem in our code during development, but it doesn't help the program take independent action to recover from individual Box errors. To allow the program to execute independent code branches based on the type of Box error thrown, we need custom BoxDimensionException subclasses (option 4).

If you want a program to differentiate among error conditions, implement a separate Error subclass for each one. Don't rely on the Error.message property alone to implement program branching logic. If your custom Error subclass defines a constructor that accepts an error string, you should use that string only for debugging, not for branching logic.


To allow our program to independently differentiate among the Box class's three error conditions, we create three custom exception types by creating three Error subclasses: BoxDimensionException , BoxUnderZeroException , and BoxOverflowException . In our case, the BoxDimensionException class extends Error directly. The BoxUnderZeroException and BoxOverflowException classes both extend BoxDimensionException because we want to differentiate these specific error types from a more general invalid dimension exception. Hence, the datatype hierarchy is shown in Figure 10-1.

Figure 10-1. The custom exception class hierarchy
figs/as2e_1001.gif

Here's the source code for our three Box Error subclasses:

   // Code in BoxDimensionException.as:   class BoxDimensionException extends Error {   public var message:String = "Illegal Box dimension specified."; }   // Code in BoxUnderZeroException.as:   class BoxUnderZeroException extends BoxDimensionException {   public var message:String = "Box dimensions must be greater than 0."; }   // Code in BoxOverflowException.as:   class BoxOverflowException extends BoxDimensionException {   public var message:String = "Box dimensions must be less "                             + "than Number.MAX_VALUE."; } 

Each class specifies the value of its message property directly and does not allow it to be customized on a per-use basis. Truth be told, now that we're dealing with class-based errors instead of string-based errors, the message property is completely secondary. What matters is the datatype of the exception generated by the throw statement. When catching any of the preceding Box exceptions, our program will use the exception's datatype (not the message property) to distinguish between the three kinds of exceptions.

Now that we have three exception types, let's update our setWidth( ) method to throw those types. Here's the code:

 public function setWidth (w:Number):Void {   if (isNaN(w)  w == null) {     throw new BoxDimensionException( );   } else if (w <= 0) {     throw new BoxUnderZeroException( );   } else if (w > Number.MAX_VALUE) {     throw new BoxOverflowException( );   }   width = w; } 

Notice that we do not pass any error description to the various Box exception constructors. Once again, the description of each exception is set by each custom Error subclass using its message property.

With each Box exception represented by its own class, the errors that can be generated by the setWidth( ) method are well-known to programmers working with Box instances. The exception types are visible outside the Box class, exposed appropriately to programmers working on the application. Just by glancing at the application class hierarchy, the programmer can determine the exceptions that relate to the Box class. Furthermore, if the programmer mistakenly uses the wrong name for an exception, the compiler generates a datatype error.

Now let's see how to add branching logic to our code based on the types of exceptions that can be generated by Box.setWidth( ) . Pay close attention to the datatype of each catch block parameter:

 var b:Box = new Box( ); var someWidth:Number = -10; try {   b.setWidth(someWidth); } catch  (e:BoxOverflowException)  {   // Handle overflow.   trace("An error occurred: " + e.message);   trace("Please specify a smaller value."); } catch  (e:BoxUnderZeroException)  {   // Handle under zero.   trace("An error occurred: " + e.message);   trace("Please specify a larger value."); } catch  (e:BoxDimensionException)  {   // Handle general dimension errors.   trace("An error occurred: " + e.message);   trace("Please specify a valid dimension."); } 

In the preceding code, if the setWidth( ) method generates a BoxOverflowException , the first catch block executes. If setWidth( ) generates a BoxUnderZeroException , the second catch block executes. And if setWidth( ) generates a BoxDimensionException , the third catch block executes. Notice that the error datatypes in the catch blocks progress from specific to general. When an exception is thrown, the catch block executed is the first one that matches the datatype of the exception.

Hence, if we changed the datatype of the first catch block parameter to BoxDimensionException , the first catch block would execute for all three kinds of exceptions! (Remember, BoxDimensionException is the superclass of both BoxUnderZeroException and BoxOverflowException , so they are considered matches for the BoxDimensionException datatype.) In fact, we could prevent all of the catch blocks from executing simply by adding a new first catch block with a parameter datatype of Error :

 try {   b.setWidth(someWidth); } catch (e:Error) {   // Handle  all  errors. No other   catch   blocks will ever execute.   trace("An error occurred:" + e.message);   trace("The first catch block handled the error."); } catch (e:BoxOverflowException) {   // Handle overflow.   trace("An error occurred: " + e.message);   trace("Please specify a smaller value."); } catch (e:BoxUnderZeroException) {   // Handle under zero.   trace("An error occurred: " + e.message);   trace("Please specify a larger value."); } catch (e:BoxDimensionException) {   // Handle general dimension errors.   trace("An error occurred: " + e.message);   trace("Please specify a valid dimension."); } 

Obviously, the addition of the first catch clause in the preceding code is self-defeating, but it does illustrate the hierarchical nature of exception handling. By placing a very generic catch block at the beginning of the catch list, we can handle all errors in a single location. Conversely, by placing a very generic catch block at the end of the catch list, we can provide a general safety net that handles any errors not caught by earlier catch blocks. For example, in the following code, the final catch block executes only if the try block generates an exception that doesn't belong to the BoxOverflowException , BoxUnderZeroException , or BoxDimensionException datatypes:

 try {   b.setWidth(someWidth); } catch (e:BoxOverflowException) {   // Handle overflow.   trace("An error occurred: " + e.message);   trace("Please specify a smaller value."); } catch (e:BoxUnderZeroException) {   // Handle under zero.   trace("An error occurred: " + e.message);   trace("Please specify a larger value."); } catch (e:BoxDimensionException) {   // Handle general dimension errors.   trace("An error occurred: " + e.message);   trace("Please specify a valid dimension."); } catch (e:Error) {   // Handle any errors that don't qualify as   BoxDimensionException   errors. } 

Remember, error granularity is a choice. In option 4 we created a custom Error subclass for each variety of exception generated by the Box class. This approach gives our program the greatest ability to respond independently to different types of errors. But such flexibility is not necessarily required in many situations. Let the needs of your program's logic dictate how granular you make your errors.

In the first release of the Flash MX 2004 authoring tool, a bug prevents catch statements from executing when the catch parameter datatype refers to a class that has been imported via the import statement. For example, in the following code, the catch statement never executes because SomeCustomError is imported:

 import somePackage.SomeCustomError; try {   throw new SomeCustomError( ); } catch(e:SomeCustomError) {   // This code should execute but it does not    // if the movie is compiled with version 7.0   // of the Flash MX 2004 authoring tool.   trace("Caught: " + e); } 


To work around the problem, we can include the full package name when specifying a custom error class as a catch block parameter. For example:

 import somePackage.SomeCustomError; try {   throw new SomeCustomError( ); } catch(e:somePackage.SomeCustomError) {   // Now that the package name is included, this code runs.   trace("Caught: " + e); } 

This bug is fixed in the Flash MX 2004 updater , available at:

http://macromedia.com/support/flash/downloads.html

 <  Day Day Up  >  


Essential ActionScript 2.0
Essential ActionScript 2.0
ISBN: 0596006527
EAN: 2147483647
Year: 2004
Pages: 177
Authors: Colin Moock

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net