C# checked exception propsal

David W. Jeske <jeske@...>
This is a proposal to add checked exceptions to C# and visual
studio. It is intended to provide many of the benefits of a checked
exception environment without the programmer annoyance present in
Java's checked exception system.

It requires these new facilities:

 (a) require "throws" declarations on all exception sources accessable
     outside the assembly (i.e. public or protected methods/properties
     of public classes or interfaces);
 (b) extend the compiler to infer "throws" information for all
     exception sources within the current assembly which do not
     contain declarations;
 (c) create formal nested exception facility;
 (d) extend the compiler to optionally derive and save exception
     source-stacks as part of the debugging information;
 (e) extend visual studio to display exception source stacks at any
     location.



Result:

     Component developers will be required to declare thrown exception
     contracts. This makes using their components easier, and makes
     catching all exceptional cases possible.

     Classes inside a single-assembly application can be left internal
     and thus they will not require throws declarations. This relieves
     the application programmer of the work of throws declarations,
     while providing typesafe exceptions. The compiler will infer
     thrown exceptions within the single-assembly, and use this
     inferred information to enforce the "throws" requirements for
     exported exception sources.

     A formal nested exception facility will guarantee it is possible
     to handle specific exceptions coming from polymorphic exception
     sources in a structured manner.

     Many existing Windows.Forms delegates would include "throws
     Exception", since this is the contract they have today. A sort
     comparator would likely not allow any exceptions to be thrown.

     Additional facilities will be available to assist in working with
     and debugging exceptions.


Here is further explanation:

  (a) require "throws" declarations on all exception sources
      accessable outside the assembly (i.e. public or protected
      methods/properties of public classes or interfaces);

      Any public or protected method of a public class is accessable
      outside the assembly, therefore, they must contain "throws"
      declarations to document which exceptions they may throw.

      Interfaces, virtual methods, and delegates are all
      "implementation contracts". Other assemblies can create
      implementations of these contracts which can appear at any call
      site for the contract. To assure that we will always know what
      exceptions can appear at one of these polymorphic callsites, the
      abstract declaration must include a "throws" declaration which
      will declare which exceptions are allowed to be
      thrown. Implementations will only be allowed to throw these
      exceptions.

      Any line of code is be capable of throwing a "RuntimeException",
      therefore, this exception type would always be unchecked and
      would never be infered or declared in a throws statement.

      In order to remain type-compatible with a published assembly,
      the throws declarations for these methods must remain the same.
 
  (b) extend the compiler to infer "throws" information for all
      exception sources with the current assembly which do not contain
      declarations;

      As long as all exception-sources which are externally accessable
      from other assemblies have "throws" declarations, it should be
      straightforward to infer throws information for all exception
      sources within the assembly currently being compiled.

      Inference for implementation contracts which are declared
      "internal" such as virtual methods, delegates, and interfaces
      must include any and all exceptions thrown by an implementation
      of the contract. As long as the contract is "internal" then
      inference can occur within the assembly.

  (c) create formal nested exception facility;

      In order to allow implementations of contacts to share
      information about the specific exception which occured while
      meeting the exception contract, the concept of nested exceptions
      must be formalized. The two enhancements required to formalize
      nested exceptions are (i) the language will assure that
      exceptions are nested, (ii) a search mechanism will allow
      scanning nested exceptions in a standard way. The following
      changes achieve these goals:

     (i) the language will assure that exceptions are nested
 
       Whenever an exception is thrown from within an exception
       handler, the compiler will force the current triggering
       exception to be nested within the new thrown exception.

       A method will be added to the base Exception class to 
       "addNested(Exception)". The compiler will automatically
       generate a call to this method as necessary. This assures
       that exceptions are free to implement their own constructors,
       and those constructors will not obscure the nested exception
       mechanism. For example:

       try {
         foo();
       } catch (MyException caught_e) {
         throw new FooException(1,2,3);

         // compiler generates:
         //
         //   Exception thrown_e = FooException(1,2,3);
         //   thrown_e.addNested(caught_e)
         //   throw thrown_e;
       }

     (ii) a search mechanism will allow scanning nested exceptions
          in a standard way.

       A method will be added to the base Exception class which
       will allow scanning of the new formal nested exception
       stack. For example:

         try { 
           foo() 
         } catch (MyException e) {
           if (e.hasNested(DBConnectFailed)) {
              // handle specific exception
           } else {
              // handle generic exception
           }
         }

  (d) extend the compiler to optionally derive and save exception
      source-stacks as part of the debugging information;

      Instead of merely tracking which exceptions are thrown from
      every method, the compiler should be capable of also tracking
      the "source call-chain" of where the exceptions are possibly
      thrown from. This information should be output as part of the
      compilation of an assembly, probably as debugger annotations in
      the assembly.

      Debugging exceptions, even with Java's checked exceptions, often
      requires writing a test-harness which will cause a particular
      exception to be thrown in order to retrieve the stack backtrace
      to figure out the call-path that exception is coming from. The
      above facility is designed to eliminate this process, by making
      this meta information available at all times.

  (e) extend visual studio to display derived throws information at
      any location

      With the information derived in (b), Visual Studio can show the
      developer exactly what exceptions can reach a given point, and
      better yet, exactly how they arrive. This will make it
      drastically easier to debug exceptions, and to assure that you
      are catching and throwing the proper exceptions.

      For example, after writing a GUI application, when you decide
      that you wish to handle exceptional cases for a button press,
      Visual Studio will show you exactly what exceptions reach the
      button handler delegate method, and allow you to resolve them
      one at a time. When you're resolving each one, you can decide
      what the proper place in the call-chain is to handle the
      exception, since the call-chain source of the exception is
      readily available in Visual Studio.

Summary:

This proposal provides the benefits of a checked exception mechanism
similar to the one present in Java, while avoiding the creation of
extra work for application developers. While it does create
slightly more work for the component developer, it also provides him
powerful facilities for understanding and debugging exceptions that he
has not had before. Ultimately, this results in components which have
well documented exception characteristics without application
developers incurring the programming-pain of Java's checked exception
system.


---------  Q & A ---------

Q: Why is Java's checked exception facility not acceptable?

A: Java's checked exception facility is rightly criticized for being
cumbersome. When developing a new piece of software, Java compilers
require you to resolve unchecked exceptions. When a complication error
occurs, the first thought of the programmer is simply to attach a
"throws" statement to the offending method. However, then the error
simply moves to the calling method, until the throws declaration has
been added to every method in the call-stack up to the main
function. Clearly this is not the right solution. The next time the
developer encounters this problem, it is solved by simply inserting an
exception handler immediately at the point where the exception
arrives. This is much faster, as it does not require another compile
and another exception handler addition for each caller in the parent
call-chain. However, this quick handler seldom performs the proper
error-handling functions. It is merely tossed in to make the program
compile. This is dangerous, as it can be a source for needless bugs
down the road. One common technique is to catch the exception and
re-throw a runtime exception. This demonstrates how Java's checked
exception mechanism is merely getting in the way of prototyping.

Q: How is this checked exception proposal an improvement?

A: This proposal has eliminated the process of declaring exception
signatures for exception sources which are internal to an assembly.
Most application developers will feel that this environment is better
than the current unchecked C# environment, because it also has (a)
good documentation of the exceptions thrown from assemblies, and (b)
good tools to help you debug and understand the exceptions present at
any point in the code. The proposal may also have an added
side-benefit of introducing more work for "public" methods, avoiding
the tendancy for component developers to "make everything public
first", "then ship", "then think".


Q: Why do we need to formalize "nested exceptions"?

A: The checked exception environment creates some challenges for
handling specific exceptions from polymorphic exception sources. A
formal nested exception mechanism allows us to handle either the
generic contract exception, or any of the specific implementation
exceptions that we know about. Any specific exceptions that we don't
know about will be handled as a generic exception.

For example, loop iterators often need to throw exceptions. Because of
this the generic iterator delegate or interface will throw a
LoopInterrupted exception. When iterator code needs to throw a
specific error, it will be nested in a LoopInterrupted exception. 


Q: How did you decide which methods require "throws" declarations?

A: All virtual methods which can be overridden or called outside the
assembly will require throws declarations. From the chart below, this
includes "protected" and "public":

                        virtual      cross-assembly override/call
-----------------------------------------------------------------
           private    not-allowed        not-allowed
          internal      allowed          not-allowed
protected internal      allowed            allowed
         protected      allowed            allowed
            public      allowed            allowed

Requiring throws declarations on "protected", "internal protected",
and "public" virtual methods is consistant with the treatment of
delegates. Namely, because virtual methods and delegates are both
explaning a type-contract that someone else may implement:

 (a) the declaration must declare what exceptions can be thrown, and
 (b) the implementor must meet the contract.

"internal" virtual methods are allowed, but are only accessable from
within the assembly. Because all implementations must occur within a
single assembly, we can infer that any exception which appears in an
override of the virtual method may appear at the virtual method call
site. This allows us to not require the "throws" declaration for these
methods.

"private" virtual methods are not allowed.

NOTE: "protected internal" means the method is accessable either from
subclasses or from the current assembly. (i.e. "protected ||
internal")

Q: Should we allow assembly wide "throws" declarations?

  As a convinence, namespaces or assemblies could be allowed to
  declare lists of exceptions which are "unchecked" during
  compliation. If necessary, the compiler would automatically add
  these exceptions to the throws clause of any exported from the
  assembly. For example, OutOfMemoryError would be a common unchecked
  exception.