Proposal: Rethrow Expressions #8740
Replies: 32 comments
-
This would seemingly fit in with #867 and #176 that would expand on the use of control statements in expressions. |
Beta Was this translation helpful? Give feedback.
-
That sounds like an incredibly narrow use case. In what situation would a developer want to preserve the stack trace if there is no inner exception, but discard all stack trace information (from both the inner and outer exceptions) if there is an inner exception, and throw that? I do not see why the language should make this very uncommon operation shorter. As for the "more general applications", do you have an example of realistic code that would be improved by this? |
Beta Was this translation helpful? Give feedback.
-
i'm with @svick on this. This seems so very narrow a use case. And the existing ways to do thsi seem more than adequate. I'm struggling to think when i'd want this. |
Beta Was this translation helpful? Give feedback.
-
@HaloFour Indeed. This may still warrant its own page, since throw expressions are already a thing. @svick I have a personal real-life use case wherein I have a dynamically emitted type that is instantiated, and a As for the other applications, the two examples given of null-coalescing operators and ternary conditionals cover two major use cases that I see - easily compacting a conditional rethrow down in much the same way as Unfortunately, it's very late where I live, so I'll come back tomorrow and see if I can't outline some more concrete examples. Use cases off the top of my head notwithstanding, not accepting the rethrow syntax is inconsistent, and does not behave as one would expect given that |
Beta Was this translation helpful? Give feedback.
-
The major issue i have is that try/catches are already so non-expressiony. You're already going full imperative to use them. So needing to put an if-statement in them seems no biggy for me. The need for my catch-clauses to be 'cute' and to have this sort of behavior has literally never come up for me in the entire time i've used C#... so i'm very skeptical about the overall value here. |
Beta Was this translation helpful? Give feedback.
-
You also have the option to use exception filters which avoids having to nest try
{
// some code that throws
}
catch (Exception ex) when condition
{
return value;
} That said, while the use case is certainly narrower it doesn't seem any more abusive than |
Beta Was this translation helpful? Give feedback.
-
Yes, but in that case, wouldn't you want to preserve the stack trace of the inner exception? You can do that by using try
{
// code that throws TypeInitializationException here
}
catch (TypeInitializationException ex) when (ex.InnerException != null)
{
ExceptionDispatchInfo.Throw(ex.InnerException);
} (The above code requires .Net Core 2.0, in older frameworks it would be slightly more verbose. I'm also not sure if the exception filter is necessary, but it's there to effectively provide the |
Beta Was this translation helpful? Give feedback.
-
I don't like the proposed syntax, and besides, you really do want to use |
Beta Was this translation helpful? Give feedback.
-
While I do agree that the throw x / throw construct does have a narrow use case that can be covered by Beyond that, I fully agree with and would like to point a finger to @HaloFour's argument pertaining to the already existing That is, if this is valid syntax, var value = possiblyNull ?? throw new ArgumentNullException(nameof(possiblyNull));
var otherValue = condition ? somethingElse : throw new ArgumentException(nameof(somethingElse));
return possiblyNull ?? throw new ArgumentNullException(nameof(possiblyNull)); why should the following, in the context of a catch block, not be valid? var value = possiblyNull ?? throw;
var otherValue = condition ? somethingElse : throw;
return possiblyNull ?? throw; Furthermore, while In terms of more concrete examples, this new syntax would serve its purpose by shortening code in cases where some sort of exception handling is taking place - say, a database transaction that fails, gets rolled back, returns a result (as an object or a booleans, not really important), and is returned or rethrown based on its success. It boils down to validation logic and handling logic in exception handlers being able to use the same new shorter syntax as the more general |
Beta Was this translation helpful? Give feedback.
-
The difference being that the use cases that 7.0 throw expression address are far more common and expected in normal non-exceptional code.
'fills in a gap' is not a reason to do something :) This goes back to the whole "the language feature has to justify its value". Just because we have something similar that works outside of catch-clauses does not mean that the language should then adopt a extension like this for very niche cases. The feature actually has to be valuable and worth it on its own merits.
This is getting more and more niche.
yes. but htat's just restating the language feature. The question is: is this an important enough case to merit all the time and effort necessary to make such a language feature? To me, it seems like it would absolutely not be the case. The benefit of throw-expressions is that they allow far more existing expression-oriented code to stay that way, while also adopting the ability to do validation and exception throwing. i.e. i can write expression oriented code with ternaries, linq-queries, patterns (out soon 'switch-expressions') that can throw in the middle if appropriate. This is valuable as expression/pattern/immutable oriented programming grows and occupies more of the mainline logic for a method. However, this feature is about extending facilities such that scenarios would be enabled only in catch clauses. Bu that point, you're already a step removed from that expression-oriented code. You're already starting a new block, and (as people have pointed out), it's not as likely to be wanting to write these sorts of cute expression operations that might then also need to do an intermediary 'throw'. -- TLDR: this feels like a feature that would be used by literally a handful of people. It would be so small as to not at all be worth it given the other features out there that would benefit orders of magnitude larger groups. |
Beta Was this translation helpful? Give feedback.
-
Note: my feeling would be different if we saw vast swaths of users having to continually write complex logic in catch clauses all over their code-base that would really benefit hugely from expression-oriented patterns. But this really isn't the case. Most code is not filled with catch clauses. And most catch clauses are fairly boring and straightforward (which is a good thing!). So, the net benefit of this change would be near nil for the vast majority of the userbase afaict. |
Beta Was this translation helpful? Give feedback.
-
You have several valid points. I don't dispute that the feature is niche, but I do think it's at least somewhat more useful than you make it out to be. Of course, that doesn't mean that I think this should be made a priority one proposal - there are, as you say, several other proposals that would be vastly more useful than this in the long run (personally, I'm very excited about the prospect of shapes). However, that doesn't, at least in my mind, preclude it from being considered at all. |
Beta Was this translation helpful? Give feedback.
-
As for "filling in a gap" not being a valid reason, I disagree. In my mind, consistency is key, both in language design and application design. Having throw expressions that are only partially functional looks and feels shoddy to me, and I wouldn't have written this proposal if I didn't have at least one use case for it. |
Beta Was this translation helpful? Give feedback.
-
Could this be solved by a |
Beta Was this translation helpful? Give feedback.
-
@GeirGrusom Elaborate? |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
If we get a |
Beta Was this translation helpful? Give feedback.
-
I'm afraid this proposal could get really confusing: try
{
}
catch (Exception1 x1)
{
try
{
}
catch (Exception2 x2) when (Nb() ?? throw) // rethrows x1 or x2?
{
}
}
bool? Nb() => ...; |
Beta Was this translation helpful? Give feedback.
-
@gafter I think the most logical answer is "rethrows |
Beta Was this translation helpful? Give feedback.
-
I wonder if it's even possible to emit valid il for that? at least, it's impossible to mimic it with today C#. |
Beta Was this translation helpful? Give feedback.
-
I think, if anything, it should be the same for break and continue, foreach (..)
{
// not sensible to say continue the current loop so, outer or error
foreach (var item in list1 ?? continue)
// actually makes sense to break out of current loop, but wouldn't be consistent with the above
foreach (var item in list2 ?? break)
} |
Beta Was this translation helpful? Give feedback.
-
@alrz I don't think |
Beta Was this translation helpful? Give feedback.
-
Consider that in But in So for that reason I would not treat them the same. |
Beta Was this translation helpful? Give feedback.
-
don't know you but I found this method easiest way to debug things... try
{
// some thing that throws exception
}
catch (Exception e)
{
Debugger.Break(); // then go and check stack trace :)
} |
Beta Was this translation helpful? Give feedback.
-
Sold. |
Beta Was this translation helpful? Give feedback.
-
There is no way to generate correct IL to rethrow |
Beta Was this translation helpful? Give feedback.
-
If you want it to throw edit: I guess there is a difference between uncaught and rethrown exceptions, but in this instance the |
Beta Was this translation helpful? Give feedback.
-
dropping |
Beta Was this translation helpful? Give feedback.
-
Would be nice to have this feature, so that we can also use it in switch expressions: 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
};
} |
Beta Was this translation helpful? Give feedback.
-
Tuples cannot be deconstructed when they are nullable. The int x;
string z;
try
{
(x,z) = CalculateValue();
}
catch (SomeException)
{
(x,z) = CalculateValueFallback() ?? throw;
} versus this with the existing syntax: int x;
string z;
try
{
(x,z) = CalculateValue();
}
catch (SomeException)
{
var fallbackValue = CalculateValueFallback();
if (fallbackValue == null)
{
throw;
}
else
{
(x,z) = fallbackValue.Value;
}
} |
Beta Was this translation helpful? Give feedback.
-
Rethrow Expressions
Summary
In C# 7.0,
throw
expressions were introduced. This proposal outlines a simple extension to the existing syntax changes, enabling not onlythrow x
as an expression, butthrow
(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 acatch
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:
Of note is also a side effect when the operand to the
throw
keyword is null, which is technically valid, that produces a thrownNullReferenceException
. 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:
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.
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).
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,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.Other than null-coalescing operators, there is also the ternary operator, which accepts throw expressions in a similar fashion.
Proposal
I propose an extension to the existing throw expression syntax, allowing
throw
, in the context of acatch
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.
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.
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.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.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.
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.
Beta Was this translation helpful? Give feedback.
All reactions