10.1. Fundamentals of the try-catch-finally Construct

Instead of listing all the permutations of the try-catch-finally construct, we will work through the combinations by starting with a routine programming problem: efficiently determining whether a string can be parsed to a double value. If you're a procedural programmer, you probably tackle this by first iterating through the characters of the string and checking whether each character is a valid numeric or decimal point or has specific symbols such as E, +, /, or -. You then check whether the decimal point and the symbols are positioned correctly inside the string and ensure that they do not occur more than once. Then you must find whether the value is within the min-max range for a double. All this sounds like coding a lot of if-else statements just to find out whether something is a valid double!

A cleaner and perhaps more object-oriented way of handling the problem is to exploit C#'s exception handling mechanism. Remember that exceptions are a breach of an assumption. So we will assume here that the string can be parsed to a double value, and then we will try to parse it. If there is a breach of our assumption, then an exception will be thrown by the .NET runtime. We will catch that exception and examine its nature. Finally, we will print some output.

Let's get our hands dirty by starting a new project and adding a code file to the project. Listing 10.1 shows what you would type in this code file.

Listing 10.1. A Simple try-catch-finally Construct (C#)
using System;

class ExceptionTest {
  static void Main(string[] args) {
    string stacktrace = null;
    double i = Double.NegativeInfinity;
    try {
      i = Double.Parse(args[0]);
    } catch (FormatException exception) {
      stacktrace = exception.StackTrace;
    } finally {
      if (stacktrace ! = null) {
        Console.WriteLine("Stacktrace is {0}", stacktrace);
      } else {
        Console.WriteLine("Double parsed "+i);
      }
    }
  }
}

The program takes the first command line argument and tries to parse it as a double (the try block). If a FormatException is thrown, then the stacktrace of the FormatException is recorded in a string (the catch block). In the finally block, the program prints the parsed integer if no FormatException was thrown; otherwise, it prints the stacktrace.

For now, it will suffice to know that C# has a System.Exception object that is the root object for all other Exception objects. This is similar to Java's java.lang.Exception class. Unlike Java, C# does not differentiate between errors and exceptions. In Java, errors are usually reserved for circumstances in which a programmer has no direct control or when the errors are something that the runtime environment should deal with. Errors are never used for control flow. Exceptions, on the other hand, can be used to alter the flow of execution.

An example of an error condition in Java is the java.lang.Virtual MachineError. Almost never thrown by the JVM, this is an error condition that typical Java programmers do not account for. Making the distinction between errors and exceptions allows Java programmers to take appropriate action in their program. Whereas an exception condition might warrant notifying the end user of a failure and leaving the end user to decide the course of action, an error condition might warrant shutting down the application gracefully.

Because C# does not make the distinction between error and exception conditions, it is in the best interest of the programmer to treat all exception and error conditions with the same rigor. C# does not have the concept of checked and unchecked exceptions.

Let's try some hands-on exception handling. Run Listing 10.1 with different command line parameters. The program prints the double value if a legal numeric value is passed. The stack trace is printed when a value containing nonnumeric characters is passed. However, if you pass in no arguments, then you will notice that the runtime pops up a dialog box (Figure 10.1).

Figure 10.1. The .NET Runtime's Way of Dealing with Unhandled Exceptions


The pop-up box is C#'s way of dealing with unhandled exceptions. It allows you to pick a debugger to further explore the kind of exception thrown. Click No for now. (We discuss debugging in later chapters.) The Visual Studio .NET 2003 IDE also allows you to set different debugging levels for the type of exception thrown. You reach this setting by clicking on the Debug menu item and then the Exceptions submenu item.

The Java equivalent of Listing 10.1 is shown in Listing 10.2.

Listing 10.2. Java Equivalent of Listing 10.1 (Java)
public class ExceptionTest {

  public static void main(String[] args) {
    double i = Double.MIN_VALUE;
    ExceptionWriter writer = new ExceptionWriter();
    String stacktrace = null;
    try {
      i = Double.parseDouble(args[0]);
    } catch (NumberFormatException e) {
      e.printStackTrace(writer);
      stacktrace = writer.getStackTrace();
    } finally {
      if (stacktrace != null) {
        System.out.println("Stacktrace is "+stacktrace);
      } else {
        System.out.println("Double parsed is "+i);
      }
    }
  }
}

class ExceptionWriter extends java.io.PrintWriter {

  public ExceptionWriter() {
    super(new java.io.StringWriter());
  }
  public String getStackTrace() {
    return ((java.io.StringWriter)out).getBuffer().toString();
  }
}

Java's exceptions don't have a convenient stacktrace property, so to obtain the stacktrace as a string we had to write a special class. The Java program behaves similarly to the C# program in terms of the command lines, but as you know, the JVM does not pop up a dialog box when the exception goes unhandled by the JVM thread running the main method.

Listings 10.1 and 10.2 contain only one method call in the try block, so it's easy to predict the number of exceptions that can be thrown. If you browse the help file in Visual Studio IDE .NET 2003, you will find out that the Double.parse (string) method throws three exceptions: Overflow Exception, FormatException, and ArgumentNullException. Thus, we can catch all three exceptions and have the stack trace printed for them.

Listing 10.3. shows a C# program that tries to catch all the possible exceptions that can be thrown by the try block. The last catch block catches the general System.Exception. Note that the most general exception must be caught in the last catch block. In this case, this exception catches the System.IndexOutOfRangeException, which gets thrown when the program is called without any command line arguments.

Listing 10.3. Catching All Possible Exceptions Thrown by the try Block (C#)
using System;

class ExceptionTest {
  static void Main(string[] args) {
    string stacktrace = null;
    double i = Double.NegativeInfinity;
    try {
      i = Double.Parse(args[0]);
    }catch (FormatException exception) {
      stacktrace = exception.StackTrace;
    }catch (OverflowException exception) {
      stacktrace = exception.StackTrace;
    }catch (ArgumentNullException exception) {
      stacktrace = exception.StackTrace;
    }catch (Exception exception) {
      stacktrace = exception.StackTrace;
    } finally {
      if (stacktrace != null) {
        Console.WriteLine("Stacktrace is {0}", stacktrace);
      } else {
        Console.WriteLine("Double parsed "+i);
      }
    }

  }
}

You can also catch specific exceptions in Java. However, because Java differentiates between exceptions and errors and furthermore between checked and unchecked exceptions, it is customary not to catch every exception thrown. Programming exceptions is more of a design issue than it seems; you have alternatives for dealing with them:

  • You can catch and log exceptions.

  • You can create custom exception objects and throw exceptions back to the called method.

  • You can let the runtime take care of exceptions.

We discuss these design issues in Section 10.5.

C# exceptions are implicitly thrown to the called method's parent method if they are not caught in the called method. Listing 10.4 shows a C# program in which the parse method is wrapped in a method that is itself wrapped in another method.

Listing 10.4. A Simple try-catch-finally Construct (C#)
using System;

class ExceptionTest {
  static void Main(string[] args) {
    string stacktrace = null;
    double i = Double.NegativeInfinity;
    try {
      i = execute(args);
    }
    catch (FormatException exception) {
      stacktrace = exception.StackTrace;
    } finally {
      if (stacktrace != null) {
        Console.WriteLine("Stacktrace is {0}", stacktrace);
      } else {
        Console.WriteLine("Double parsed "+i);
      }
    }

  }

  public static double execute(string[] args) {
    return =_execute(args);
  }
  private static double =_execute(string[] args) {
    return Double.Parse(args[0]);
  }

}

In Listing 10.4 we have merely moved the Double.Parse(string) call two functions deep. Notice that this program behaves similarly to Listing 10.1.

In C#, uncaught exceptions thrown by a method are passed to the parent method (the method that called the current method). In this case, the exception was thrown by the _execute() method, which was then passed to the execute() method, which passed the exception to the Main() method. The passing of exceptions bubbles up the method stack until it reaches the root method of execution, at which point the runtime takes over and pops up the dialog box. This bubbling up of the exception through the method stack is also present in Java.

You can nest try-catch-finally blocks as long as you account for every try block with a catch block or a finally block. If an exception is not caught by the inner try-catch block, then it is tried by the outer try-catch block. Listing 10.5 illustrates this point.

Listing 10.5. Nesting try-catch Blocks (C#)
using System;

class ExceptionTest {
  static void Main(string[] args) {
    string stacktrace = null;
    double i = Double.NegativeInfinity;
    try {
      try {
        i = Double.Parse(args[0]);
      } catch (ArgumentNullException e) {
        Console.WriteLine("Exception is {0}", e);
      }
    }catch (FormatException exception) {
      stacktrace = exception.StackTrace;
    } finally {
      if (stacktrace != null) {
        Console.WriteLine("Stacktrace is {0}", stacktrace);
      } else {
        Console.WriteLine("Double parsed "+i);
      }
    }

  }
}

When Listing 10.5 is run with a command line parameter that is nonnumeric, the inner try-catch block cannot catch the exception, and hence the outer try-catch block catches the exception. Now test this program by changing the inner try block to the following:

i = Double.Parse(null);

Now the inner try block catches the ArgumentNullException.

In nested try-catch blocks, the inner blocks can catch the more general exception. Although this is a bad coding practice, it can be done. Java supports nested exceptions in the same way as C#, and the inner exceptions can be general exceptions.

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

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