Skip to content

Instantly share code, notes, and snippets.

@Nihlus
Created February 25, 2018 17:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Nihlus/d71a9c39c4cc5c838ac259aa8eee137c to your computer and use it in GitHub Desktop.
Save Nihlus/d71a9c39c4cc5c838ac259aa8eee137c to your computer and use it in GitHub Desktop.
Rethrow Expressions

Rethrow Expressions

Summary

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.

Motivation

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.

Detailed design

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.

Background

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;
}

Proposal

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:

  1. throw is now considered an expression, and can now be used as the right-hand operator in a null-coalescing construct.
  2. 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.
  3. 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.

Implementation

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;
}

Drawbacks

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.

Alternatives

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.

Unresolved questions

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.

@AroglDarthu
Copy link

AroglDarthu commented Apr 23, 2021

In the new switch expression syntax it could also be useful:

try
{
    DoSomething();
}
catch (FlurlHttpException flurlHttpException)
{
    var statusCode = flurlHttpException.Call?.HttpResponseMessage?.StatusCode;
    throw statusCode switch
    {
        HttpStatusCode.NotFound => new MyNotFoundException(),
        HttpStatusCode.Conflict => new MyConflictException(),
        HttpStatusCode.NotAcceptable => new MyNotAcceptableException(),
        _ => throw
    };
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment