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.