- Proposed
- Prototype: Complete
- Implementation: In Progress
- Specification: Not Started
In C# 7.0, throw
expressions were introduced. This proposal outlines a simple extension to the existing syntax changes, enabling not only throw x
as an expression, but throw
(in the context of rethrowing a caught exception) as well.
In the current iteration of the language, throw
expressions are widely used to conditionally select either a value or expression, or throwing an exception. The syntax that is already in place allows simple and terse usage of these patterns, however, in its design one pattern was left behind: throw
without an argument, inside a catch
block.
Enabling its use in similar fashion to the C# 7.0 throw expressions fills in the gap left behind, and completes the expected syntax for throw
expressions. Furthermore, it makes it far easier for developers using the new syntax to utilize it in a way that preserves stack traces of caught exceptions.
In principle, this proposal is a simple extension to exception rethrowing syntax when used in combination with null-coalescing and ternary operators. At present, the available syntax when combining the two is somewhat limited, but presents an opportunity for a small yet useful improvement.
First, some background. If you consider yourself versed in C# exception handling, you can skip ahead to Proposal.
When exceptions are thrown and caught in the language using try
/catch
blocks, exceptions can be swallowed and/or handled, rethrown, or new exceptions can be thrown. Depending on the syntax used, the stack trace of the exception is preserved or destroyed.
Exceptions can also contain inner exceptions, which are defined to be the cause of the outer exception. A commonly seen use of this is in the TargetInvocationException
class, which automatically wraps exceptions thrown in constructors.
Some examples of the various ways exceptions can be manipulated in these ways:
try
{
// some code that throws
}
catch (Exception ex)
{
// handle the exception cleanly
}
try
{
// some code that throws
}
catch (Exception ex)
{
throw; // rethrow the exception, preserving the stack trace
}
try
{
// some code that throws
}
catch (Exception ex)
{
throw ex; // rethrow the exception, destroying the stack trace and creating a new one from this point
}
try
{
// some code that throws
}
catch (Exception ex)
{
throw ex.InnerException; // rethrow the inner exception, creating a new stack trace from this point
}
Of note is also a side effect when the operand to the throw
keyword is null, which is technically valid, that produces a thrown NullReferenceException
. It is important to be aware of this behaviour in the context of this proposal, and in normal code.
Particularly, in the example above, if no inner exception is present, a NullReferenceException
will be thrown instead, whereas it may be preferable to handle the lack of an inner exception differently (either by rethrowing the outer exception directly, or cleanly handling the case where no inner exception is present). This behaviour, while typically avoided in robust code, is within the scope of this proposal as a factor for simplifying its avoidance.
As a final example, the behaviour can be explicitly triggered using the following pattern:
try
{
// some code that throws
}
catch (Exception ex)
{
throw null;
}
The language, in its current iteration, has a method for handling the not null/null case when dealing with instances of objects: the null coalescing operator. This operator is a quick shorthand for inspecting a reference, determining if it is null, and selecting either the original reference or an alternative value in the case of null.
var value = possiblyNull ?? alternativeValue;
In the compiler, this is lowered into a simple if statement (example lowered code may not be exactly accurate, but near enough for the core concept).
var value = possiblyNull;
if (value is null)
{
value = alternativeValue;
}
Exceptions, being instances of objects, are subject to the same syntax. '
An extension to their use in null-coalescing operators was made in C# 7.0, wherein new syntax was introduced that considers the throw x
syntax as an expression, allowing it to be used as the right-hand operand in null-coalescing operators, that is,
var value = possiblyNull ?? throw new RelevantException();
throw
expressions may also appear as the left-hand operator in null-coalescing statements, but can only be combined with either a value, or another throw expression with an exception instance argument.
try
{
// some code that throws
}
catch (Exception ex)
{
throw ex.InnerException ?? throw ex;
}
Other than null-coalescing operators, there is also the ternary operator, which accepts throw expressions in a similar fashion.
try
{
// some code that throws
}
catch (Exception ex)
{
return condition ? value : throw ex;
}
I propose an extension to the existing throw expression syntax, allowing throw
, in the context of a catch
block and without an argument, to be treated as an expression.
With the extension, throw expression, ternary- and null-coalescing operator syntax is available that allows their combination into terse constructs.
One, a construct that throws a given expression object if it is not null, and rethrows the existing exception if it is, preserving the stack trace.
try
{
// some code that throws
}
catch (Exception ex)
{
throw ex.InnerException ?? throw;
}
This construct, while not currently valid syntax, is a logical extension to existing solutions and follows an easily readable format.
In particular, this would be a useful shorthand for unwrapping exceptions with potential causational exceptions, where the developer would want to preserve the stack trace in the case of a missing inner exception, and begin a new one if not.
Practically, this change would be applicable not only to exception unwrapping, but also to more general applications, in similar vein to the current throw expressions.
try
{
// some code that throws
}
catch (Exception ex)
{
var value = possiblyNull ?? throw;
}
try
{
// some code that throws
}
catch (Exception ex)
{
return condition ? value : throw;
}
Effectively, this means a few things:
throw
is now considered an expression, and can now be used as the right-hand operator in a null-coalescing construct.- In null-coalescing constructs, if the left-hand operator is a
throw new
expression, its target exception reference is checked for null, and if it is null, the right throw expression is selected. If not, the left throw expression is selected. This is already the existing behaviour, and simply needs to be extended to accept 1. - In ternary conditionals, if the left-hand operator is an and the right-hand operator is a
throw
, the expression is either chosen, or the current exception is rethrown.
In similar fashion to the existing implementation of the null-coalescing operator, the construct can be implemented using simple compiler lowering. The above examples would, after lowering, be transformed into C# syntax valid in the current language version.
try
{
// some code that throws
}
catch (Exception ex)
{
if (ex.InnerException is null) // or == null, if operator overloading semantics are to be preserved
{
throw;
}
throw ex.InnerException;
}
try
{
// some code that throws
}
catch (Exception ex)
{
if (conditon)
{
return value; // or whichever left-hand expression was passed to the ternary
}
throw;
}
I see no major drawbacks to implementing this proposal. It does not, as far as I can tell, render any existing constructs invalid or incompatible, nor does it alter existing behaviour.
However, I'm certain there will be additonal considerations to take into account once the proposal has been explored further.
As discussed above, there are equivalent constructs already available to developers in the form of simple if
conditionals. Much in the same way as ternaries and null-conditionals simplify code for developers, this proposal's intent is to simplify that very construct.
However, for the sake of clarity, I will refer back to a previous section for the two most common examples of equivalent syntax - the null-coalescing operator, and the ternary return conditional. See Implementation.
I'm quite sure more applications for this new syntax (and more issues to consider) will rear their head once it's been explored further. What other instances can the new syntax be used in? What side effects could it have? How difficult will this lowering step be to implement?
Unfortunately, I am not a compiler developer, and cannot answer most of these questions beyond what I have already outlined.
As such, thoughts, input, and suggestions are very welcome.
In the new
switch expression
syntax it could also be useful: