Exploring future of try expression #2734
Replies: 19 comments 22 replies
-
Some of this sounds similar to Midori's dataflow-oriented syntax sugar around try/catch. |
Beta Was this translation helpful? Give feedback.
-
BTW, the initial version of the third syntax was proposed by @Pxtl here- #980 (comment) |
Beta Was this translation helpful? Give feedback.
-
None of the proposed syntaxes are functionally equivalent to your current syntax example.. The example has the |
Beta Was this translation helpful? Give feedback.
-
I don't think any of the
The |
Beta Was this translation helpful? Give feedback.
-
I prefer option 1 because it would be almost the same as Another option. Maybe var user = Authenticate(cred) switch {
case {} u => u,
catch ValidationException e => Logger.LogExceptionAndReturnNull("Invalid credential", e),
catch AuthenticationException e => Logger.LogExceptionAndReturnNull("Inauthentic credential", e),
finally => ReleaseSomething()
} |
Beta Was this translation helpful? Give feedback.
-
I like the concept, but I'm just a bit curious what the type of |
Beta Was this translation helpful? Give feedback.
-
@Hosch250 C# at the moment can use object obj = DoSomeThing();
switch(obj)
{
case int i: return "number : " + i.ToString();
case long l: return "number : " + l.ToString();
case float f: return "number : " + f.ToString();
case string s: return "string : " + s.ToString();
} |
Beta Was this translation helpful? Give feedback.
-
@Thaina I believe @Hosch250 was referring to the type that would be inferred for I can't say that I'm a big fan of the idea myself, but I can see why some might find this syntax useful. |
Beta Was this translation helpful? Give feedback.
-
Ah, it was in the proposal, I just missed it. I was thinking about the following structure:
I missed where it said the pros/cons were the use of a |
Beta Was this translation helpful? Give feedback.
-
I think @DanFTRX raises a great point, and in general this seems to create more problems than it solves. In fact, I'm not actually sure what problem it is meant to solve: I can't think of a use-case for what amounts to conflating assignment and non-local, non-block-level exception handling. Crucially, what happens to unhandled exceptions? Should it just float around, quietly put on hold in this How does this work if you need a In the unlikely event somebody has a real need for this, you can handle it yourself by returning a tuple like |
Beta Was this translation helpful? Give feedback.
-
Personally, I strongly want this. I often find the absence of this feature causes the constant repetition of the variable name in declare varname => try => set varname value. If a method throws an exception and I want to handle it but still return an object to the caller, this is needed. Definitely prefer edit: small correction in |
Beta Was this translation helpful? Give feedback.
-
I also feel a need for try expression and agree to @Pxtl I really long the days I do not have to scope some variable outside try/catch block, because I want localized try/catch blocks so further statements get their own exception handlers, but I want to reuse result from the previous try/catch. In F# world this is called try..with expression. There are tons of proposals about single line try/catch statements, expressions and whatnot - most of them closed because they encourage swallowing exception. However this discussion/proposal could actually invite handling the exceptions in a stronger way than before: try without catch or without explicit discard pattern should do I believe implicitly throwing if discard pattern case not provided is acceptable as currently functions with return value can be escaped the same way - if exception is thrown or one doesn't catch an exception. Regarding
|
Beta Was this translation helpful? Give feedback.
-
I'm still not clear what problem is being solved by any of this. Or, for that matter, why these kinds of things are entertained, when other ideas are tossed because C# supposedly follows a tradition of C-like syntax, which this and already-implemented things like But mostly I'm struggling to see the value-add... |
Beta Was this translation helpful? Give feedback.
-
I believe the purpose of a try expression would be to simplify the ceremonial code around trying the execution of a simple expression, and handling exceptions at a fundamental basis, and quickly using that result of the execution, or ignoring it if failed. Otherwise, should things get more complex, the standard try statement is always preferred, as it's cleaner and clearer of purpose. To demonstrate, say you want one database operation to be performed, and log the exception if thrown, without caring about its type because it's unexpected. The syntax I'd strive for is something like this: var result =
try ExecuteOperation(args)
catch e => logger.ErrorException(e); The above pretty conveniently conveys the meaning for a simple invocation and the logging of an exception that we don't evaluate after being thrown. Now, for something that would evaluate the exception, a switch-like syntax would be appropriate after the catch keyword, like this: var result =
try ExecuteOperation(args)
catch
{
ArgumentException or
NotSupportedException => throw,
Exception e => logger.ErrorException(e)
}; As for the final result of the expression, if the tried expression succeeds, it would be the result of that expression. Otherwise, if we roll into the catch area, the default value of the target type is returned. However, it would be ideal to customize that, so something like the following could suit: var result =
try ExecuteOperation(args)
catch e
{
ArgumentException or
NotSupportedException => throw,
InvalidOperationException ioe =>
{
return ExceptionResult.Invalid(ioe);
},
_ => logger.ErrorException(e),
}
into new ExceptionResult(e); The above example would return That's about as far as I'd personally go with such an expression, before starting to think hard how to write it beautifully. |
Beta Was this translation helpful? Give feedback.
-
I have developed some new perspectives on exception handling in C#, or languages with Exception in general. I think, exceptions are usually caught to do one of the following-
In my opinion, and "expression" makes sense for the first 4 types of handling. And instead of some variant of var result =
NetworkCall()
>|| retry { on: IOException, times: 3, delay: 50 }
>|| AnotherNetworkCall()
>|| retry // implying retry once without delay on all exceptions
>|| ReadFromFile()
>|| default("A default value") { on: IOException };
// work with result The operator For the point 5, logging/recording exceptions, I think it is a cross cutting concern. And some ways should be provided to log all exceptions in an application, at the caught-site, or at the crash-site. JS ecosystems provide something like this using events. For the point 6, the swallowing of exceptions should be done with caution. This should only be done at the root of some workflow, to prevent crashing of the app/thread, which will prevent affecting other workflows. This is my current opinion, which obviously can change in future. Edit: Exception type filter for default. Edit 2: I was trying to figure out what it would be translated to in current C#. I could not come up with anything without using string result;
int retries;
retries = 3;
while (true) {
try {
result = NetworkCall();
goto success;
} catch(IOException) {
if (--retries == 0){
goto step2;
}
else Thread.Sleep(50);
} catch {
goto step2;
}
}
step2:
retries = 1;
while (true) {
try {
result = AnotherNetworkCall();
goto success;
} catch {
if (--retries == 0) goto step3;
}
}
step3:
try {
result = ReadFromFile();
goto success;
} catch(IOException) {
result = "A default value";
goto success;
}
success:
// work with result |
Beta Was this translation helpful? Give feedback.
-
I've already chimed in on this, but imho too many approaches here are trying to reinvent the wheel. C# already has a lambda syntax that includes both single-line expressions and multi-line expressions that have side-effects. While it may defeat the spirit of the "try expression", I see no reason why it shouldn't be allowed. It solves the "try-with-log" common case, albeit in an ugly way. This lambda syntax works fine, and any "try expression" should just use that for catch-expressions. Inferring default for expressions that return void might look pretty, but it's not idiomatic C#. For a catch expression to be valid, it should return the same type as the try, or it should throw. (note, my syntax uses a single catch with each type separated by commas instead of multiple catch statements to avoid the ambiguous "which try is this a catch for" problem that could be caused by nested unbraced try and catch statements). var responseCode = try(DoHttpRequest().ReponseCode) catch (
NotFoundException => 404, //don't need ex variable, use immediate value.
HttpException ex when (ex.Code < 500) => ex.Code, //just pass-through the response-code
HttpException ex => { //for 500 and up HTTP errors, log the error before returning
logger.Error(ex);
return ex.Code;
},
SomeSpecialCaseException ex => throw MyWrappingExceptionClass(ex), //this is a special case where there's a higher-level error handler that needs to tackle it
Exception ex => { //treat all other errors as 500s.
logger.Error(ex);
return 500;
}
); |
Beta Was this translation helpful? Give feedback.
-
While I like @Pxtl's suggestion the most so far, I wanted to comment on two things that affect most suggestions. First, I think that you could completely skip the var _ = SomeFunction().SomeIntProperty catch {
FileNotFoundException => -2,
InvalidOperationException => -3,
CustomException ce when (ce.SomeIndicator is false) => -4,
_ => throw, // The `throw` keyword should imo be available, just as it is in a regular `catch` clause
}; I think that this construct is uniquely identifiable and should not cause any parsing ambiguities. Feel free to point out something I'm missing. I also think that no legibility is lost when omitting the I also think that this should be somewhat simple to implement, since any construct of this form <statement>(<try-expression> catch {
<caught-exception-1> => <result-expression-1>,
<caught-exception-2> => <result-expression-2>,
<caught-exception-3> => <result-expression-3>,
...
_ => <default-result-expression>,
}) should be translatable to this <inferred-type> __value;
try {
__value = <try-expression>;
} catch (<caught-exception-1>) {
__value = <result-expression-1>;
} catch (<caught-exception-2>) {
__value = <result-expression-2>;
} catch (<caught-exception-3>) {
__value = <result-expression-3>;
}
...
catch {
__value = <default-result-expression>;
}
<statement>(__value) Second, since multiple people have mused about how to use More broadly, I saw some people here make the argument that this idea lacks merit because it can't handle scenario X or Y of the regular public int SomeValue {
get {
try {
return SomeFunction().SomeProperty;
} catch {
return -1;
}
}
} To this: public int SomeValue => SomeFunction().SomeProperty catch { _ => -1 }; Or maybe even this, if we also allow a shorthand for catch-all exceptions: public int SomeValue => SomeFunction().SomeProperty catch -1; All without a loss of clarity. I think every current C# developer would be able to grok the above code with a cursory reading. And finally, to the people arguing against this suggestion because they don't see the merit in it - brevity may not be a virtue in every situation, but if it preserves clarity and legibility, it has many advantages over longer code. Less room for bugs, faster to type, to read, less vertical space wasted, less scrolling, etc. There's a reason why expression-bodied everything has been getting so popular. |
Beta Was this translation helpful? Give feedback.
-
Personally, I'm not a fan of a
🚫 Exceptions are meant to be exceptionalUnfortunately, many developers do not attempt to prevent exceptions but rather simply handle them. However, most exceptions can be prevented through validation; and those that cannot, should (context aside), terminate the current process early. A good example is attempting to read all text from a file: string content = await ReadAllTextFromFile(filePath);
...
private bool TryValidateFilePath(string filePath)
{
if (string.IsNullOrWhiteSpace(filePath))
{
// would throw an argument null exception
_logger.LogError("Unable to load samples because the file path is empty.");
return false;
}
var invalidCharacters = Path.GetInvalidPathChars();
if (invalidCharacters.Any(filePath.Contains))
{
// helps prevent not supported exception, but doesn't ensure it won't happen
_logger.LogError("Unable to load samples because the file path contains invalid characters.");
return false;
}
var filePathAttributes = File.GetAttributes(filePath);
if (filePathAttributes.HasFlag(FileAttributes.Directory))
{
// unauthorized access exception can be thrown for trying to read a directory instead of a file
_logger.LogError("Unable to load samples because the file path is a directory, not a file.");
return false;
}
return true;
}
private Task<string> ReadAllTextFromFile(string filePath)
{
try
{
if (!TryValidateFilePath(filePath))
return Task.FromResult<string>(null);
string result = File.ReadAllText(filePath);
return Task.FromResult(result);
}
catch (PathTooLongException)
{
_logger.LogError("Unable to read text from the specified file path because the path is too long.");
}
catch (NotSupportedException)
{
_logger.LogError(
"Unable to read text from the specified file path because the format of the path it is not supported.");
}
catch (Exception e) when (e is DirectoryNotFoundException or FileNotFoundException)
{
_logger.LogError(
"Unable to read text from the specified file path because the directory was not found.");
}
catch (Exception e) when (e is UnauthorizedAccessException or SecurityException)
{
_logger.LogError(
"Unable to read text from the specified file path because the file is read-only or the current user does not have access.");
}
catch (Exception e)
{
_logger.LogError(
"Unable to read text from the specified fil path due to an unexpected issue. {ExceptionMessage}",
e.Message);
}
return Task.FromResult<string>(null);
} ✅ My recommendation for a
|
Beta Was this translation helpful? Give feedback.
-
Isn't it possible to implement a JavaScript-like way of doing catch? For example you have an async function |
Beta Was this translation helpful? Give feedback.
-
There was a discussion regarding what try expressions may look like two years ago. Since then, a change has been made in C# language- how match or switch expression looks like. With the chosen syntax of switch expression, most of the then proposals of try expression syntax are outdated now. I am not proposing a single try expression syntax here. Rather I am trying to explore different paths try expression can take in future for C#. I can basically imagine four syntaxes for try expression. To describe them, I want to use the following snippet with current try-catch syntax, which then will be translated to syntax of the respective proposals. Given a method,
User Authenticate(Credential credential)
which may throwValidationException
orAuthenticationException
-try
expressiontry
expression evaluated to aResult
object combining success and exception cases. Thenswitch
expression can be applied on the result object. Given example will be-Or it can be-
Or it can be-
Notice calling
LogExceptionAndReturnNull
method in some cases, as all arms of switch expression have to return value of same type.Pros:
Cons:
try ... switch
expressionThe
try ... switch
expression will be extending the current switch syntax to catch and handle exception cases, when input is given with atry
. Given example will be-Or it can be-
LogExceptionAndReturnNull
has been used here also.Pros:
default
or_
arm/case will also swallow all other exception.Cons:
default
or_
case is doing with exceptions not already handled.try ... catch
expression 1Here
catch
part will use same syntax as currentswitch
, but will catch and handle only exception cases. Unhandled exceptions will be thrown. The result in the success case can be handled in any way necessary. Given example will be-Or it can be-
LogExceptionAndReturnNull
has been used here as well.Pros:
Cons:
try ... catch
expression 2This is same as the previous one, except catch parts does not have to (or cannot) supply a default value. For all handled exceptions, the default value is simply
null
. The??
operator can be used to provide other default values. Like previous one, unhandled exceptions will be thrown. So here arms/cases of the catch do not contribute to the type of resultant expression. Given example will be-Or it can be-
Pros:
Cons:
For last two proposals, I don't like allowing a swallow-all
_
or default case. But someone can be for it. Personally I like the last syntax. Let's discuss which one do you like and why. Did I miss pros or cons of any syntax? And also, can there be any other syntax for try expression?Some related issues and discussions can be found in #1019 and other issues it referenced.
Beta Was this translation helpful? Give feedback.
All reactions