-
Notifications
You must be signed in to change notification settings - Fork 4.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: Declarative approach to exception handling #9539
Comments
I don't see why moving the exception handling outside of the method would make it easier to read. If anything it makes it more complicated since you then have to discern how the compiler might wire up the different handlers and then actually follow the handlers to where they perform their action. This type of syntax also completely precludes the ability to use exception filters ( I don't particularly care for the scope guard syntax of languages like D, for the same reasons that I don't like You also imply automatic disposal of |
The one use-case I could see here is not having to write repetitive Too often I see this repeated ad nauseum |
I could see something like this being useful for reusable exception handling. In the following example I reuse an exception handler. I tweaked the syntax a bit, to exception handler classes instead of methods. public class ExceptionLogger : IExceptionHandler<Exception>
{
Logger _logger = new Logger();
public void Handle(Exception ex)
{
_logger.Log(ex);
}
}
public class FooExceptionHandler : IExceptionHandler<FooException>
{
public void Handle(FooException ex)
{
// handle exception
}
}
public class C1
{
public void DoStuff()
{
try using ExceptionLogger
{
var io = new IO();
result = io.DoSomething<T>();
}
}
}
public class C2
{
public void Go()
{
try
using ExceptionLogger,
using FooExceptionHandler
{
var io = new IO();
result = io.DoSomething<T>();
}
finally
{
Console.WriteLine("Done with 'Go'.");
}
}
} The latter example shows how you could chain exception handlers if you wanted to handle multiple types of exceptions, or if you wanted multiple handlers for different purposes for the same exception type. I suppose you could even mix them up (this has the same behavior as the latter example): try using ExceptionLogger
{
var io = new IO();
result = io.DoSomething<T>();
}
catch (FooException ex)
{
// handle exception
}
finally
{
Console.WriteLine("Done with 'Go'.");
} |
Well, it depends what you read so I don't really understand why would it make things more complicated... I mean if you're trying to understand what a method does then exception handling isn't part of what the method does and when I'm reading a method I don't really care how it handles exceptions. If you do care about exception handling then you can easily read the method responsible for it. It's easy to write a static class and name it something like IOExceptionHandler and put all the related methods there for exception handling, it can be a nested class or non-nested one, depends on reusability and your needs. The syntax can be iterated on and improved to make sense, I'm merely stating an idea here. :)
Well, this is partially why I didn't suggest the exact same features, the approach I'm suggesting is quite different and it's easy to follow the order of execution and the rules are quite simple because the order is actually kept! it's everything between expect and exit, now I don't really like the notion of exit at all myself but this is what I had in mind at the time of posting, however, I do like the ability to break the concerns and delegate the exception handling to another method or methods when it make sense.
Well, some rules needs to be in place, just like we have a well defined behaviour for using, that basically calls dispose automatically when objects get out of scope and when this doesn't work, I don't know how everyone are handling their exceptions and what their techniques for it outside to the code I'm working on and open source projects but having methods cluttered with try/catch and multiple catch statements isn't too rare and I don't think it's very pleasant. Making it optional shouldn't be a problem, I mean it's possible to exit without providing an object so in this case it won't call dispose. @whoisj That's exactly my point, making methods DRY as possible and allow more reusable exception handling. I really like the ability to have exceptions delegated than handling them in the same method because it makes the intent quite clear, at least the way I think about it. |
The only question I'd have (regarding my tweaks) is how to public ExceptionReturnBehavior Handle(Exception ex)
{
_logger.Log(ex);
if (condition) return ExceptionReturnBehavior.Throw;
else return ExceptionReturnBehavior.Continue;
} Or maybe go so far as to have a special syntax sugar: public exception handler ExceptionLogger
{
Logger _logger = new Logger();
catch (Exception ex)
{
_logger.Log(ex);
throw;
}
} which just translates into a class similar to what I wrote above, but is more natural for the purpose. This could address @HaloFour's concern about using |
How a method handles its exception is exactly part of what that method does. Even if it's reusing some logic from elsewhere, the method has to opt-into doing it, and nothing stops you from calling some reusable method in your exception handler now. Any syntax that you could propose would be significantly crippled compared to proper exception handling. You immediately lose all of the scoping that a normal exception handler would have. Method arguments, gone. Locals, gone. You lose all capacity for method control within this handler. No |
@HaloFour sometimes you need to lose something to gain something else, this isn't replacements for all scenarios or removal of the try/catch block, just like you don't use the using statement for no reason, you won't be using it when it's not necessary. If your exception handler is doing so little that this functionality fits the bill then you're probably doing exception handling wrong anyway. Logging is pretty common scenario that generally all it takes is one call, it's too little and yet people are doing it and I don't think it's wrong. If you're doing too much inside your exception handling than your method is no longer doing its job but becoming something else completely, SRP applies to classes level but it can certainly apply to your operations too. |
Logging and rethrowing is an anti-pattern (and something that this feature can't even accomplish due to the inability to use the IL opcode |
@HaloFour fair enough, I agree with most of what you wrote but logging and rethrowing sometimes make sense iff we can't catch the information farther in the stack but yeah, even in this case we can throw a new a exception and pass the current one as an inner exception and finally log everything in a centralized place so I guess we're in agreement on this. What really bothers me with the current approach is I feel like it's wrong that whenever I need to handle a new exception I need to change the method responsible for raising it, I guess that this coupling between the method logic and exception handling is noise I don't really want to have, another point is that it's quite verbose and it can easily turn simple things to look like a mess. Control flows are great when you want to focus on the trees but they aren't so great when you want to see the forest, I guess that's what I'm trying to capture here but then again, I'm not a fan of the approach I took initially so yeah, I need to think about it some more and take into consideration the points people made here. |
Related: #4890 |
The suggestion(s) are not declarative but are pretty imperative by the way. Declarative is something like this (it's not about custom attribute but rather the form):
Java's |
@dsaf it depends how you look at it. The declarative part is really the point where you don't need to provide catch statements as a control flow, you just need provide the method that handles it, so in that sense you specify what you want to catch and let the compiler deal with how to structure it. In computer science, declarative programming is a programming paradigm—a style of building the structure and elements of computer programs—that expresses the logic of a computation without describing its control flow. Now, you can claim that there's some sense of control flow here and I totally agree and it's kinda implicit with the exit statement which I really dislike.
That's not the only way to do declarative programming, I know you gave that as an example but mine is no different. P.S. I'm not sure what's so declarative with Java's throws, it's just a hint. |
Some alternatives for discussion. Monadic form implementable today: return ExceptionMonad
.With(new IO())
.Try(io => io.DoSomething<T>())
.Catch<IOException>(ExceptionHandler)
.Catch<Exception>(ExceptionHandler)
.Finally(io => io?.Dispose())
.ReturnOrRethrow(); Minimal try-catch syntax extension: public T SomeIOoperation<T>() where T: class
{
IO io;
try
{
io = new IO();
return io.DoSomething<T>();
}
catch (IOException ex) => ExceptionHandler; //New expression + method group syntax.
catch (Exception ex) => ExceptionHandler; //New expression + method group syntax.
finally io?.Dispose()
return default(T);
} Same refactored with public T SomeIOoperation<T>() where T: class
{
//New syntax - no curly brackets for try.
try using (var io = new IO()) return io.DoSomething<T>();
catch (IOException ex) => ExceptionHandler; //New expression + method group syntax.
catch (Exception ex) => ExceptionHandler; //New expression + method group syntax.
return default(T);
} |
@dsaf I actually love the suggestions because it kinda solve most of the things @HaloFour was speaking about. The syntax is much more terse than the current version and can be nice to work with. Here are few more ideas based on your suggestions: public T SomeIOoperation<T>() where T: class
{
IO io;
try
{
io = new IO();
return io.DoSomething<T>();
}
catch (IOException ex) => ExceptionHandler; //New expression + method group syntax.
catch (Exception ex) => ExceptionHandler; //New expression + method group syntax.
finally io //Dispose is called implicitly.
return default(T);
} Here is one more suggestion: public T SomeIOoperation<T>() where T: class
{
IO io;
try
{
io = new IO();
return io.DoSomething<T>();
}
catch ExceptionHandler //Points to a method group, single catch statement.
finally io
return default(T);
} |
Adding expression-bodies to more statements has already been brought up and the LDM stated that they didn't desire to do so outside of method bodies, so I don't think that Relevant comment: #7881 (comment) I don't really see what the rest of that syntax buys over what you have today: public T SomeIOoperation<T>() where T: class
{
using (IO io = new IO()) try // formatter will fight you, but this is legal syntax
{
return io.DoSomething<T>();
}
catch (IOException ex) { ExceptionHandler(ex); }
catch (Exception ex) { ExceptionHandler(ex); }
return default(T);
} |
@HaloFour Well, it the same reason why we have expression bodies on methods, it makes the syntax terse and more pleasant though I see the point, having too much of these can do the exact opposite. |
I am personally only using expression bodies on truly one-liner methods - sparingly. |
@HaloFour curly brackets could be made optional without expressions. Although I am neutral on that matter. I think Microsoft's guideline enforces curly brackets even on |
@dsaf I know that with |
disclaimer: I'm not a compiler guy so I have no clue what are the challenges (if any) to implement something like this as part of the language but I'm really curious and I would love to hear more about it from anyone in the community or the devs.
TL;DR Can we come up with ways to declarative approach to do exception handling? meaning that when we read a method, exception handling isn't getting in our way to understand it.
Note! this proposal isn't meant to be a replacement for the traditional try/catch/finally block but a syntactic sugar, so if you need more control, you can always fall back to the traditional way of doing it, choose the right tool for the job.
The technique I propose below was inspired by Job Kalb approach to refactor exception handling from methods in order to centralize it and make it reusable aka lippincott function, I just built on this idea with some more stuff.
I've written quite a bit of C# code over a decade and atm I'm writing more C++ code and in my research for exception handling in C++ for an application I'm writing I've discovered how to do exception handling in a more declarative way and because I really love C# when I do things in other languages or inspired by other languages I tend to think about how it would exist in C# just because..
I've watched these talks from Andrei Alexandrescu and Jon Kalb and some of the techniques they speak about are extremely powerful because they completely avoid the need for control flows especially when it comes to nested code and the nice thing about it is that it makes simple things simple to read and reason about.
They are speaking about exception handling in the context of C++ but I think that some of the concepts are equally viable in C# or any other language but I see no straightforward way to implement it in C# without winding the stack and keep the usage as simple as it is in C++ or D.
Currently to handle exceptions in C# we have the usual try/catch/finally control flow that adds quite a bit of noise to the function and hurts readability.
Now, what I'm suggesting is something like the following:
Updated the proposal based on the replies below and with some of my own thoughts.
How it works?
You define a single
catch
statement with the name of the exception handler, for each exception you want to handle you just overload the handler with the exception type as the first parameter.It should be possible to encapsulate these handlers in a static class to create reusable handlers, like so:
What would the compiler generate?
The code that would be generated by the compiler is something like the following:
I'm not sure whether the call to
ExceptionHandler
is even necessary, maybe the compiler can inline this or something...Advantages:
Disadvantages (Problems?):
Because the actual handling of the exception is done outside to the original function (caller) that raises the exception there are few downsides to this approach:
return
out of the caller.break
orcontinue
in the caller.throw
; however,throw ex
is possible.I don't know how to solve all these problems and I don't know whether they are actually problems but for this proposal to be useful the first and the last points are definitely important issues in my opinion.
More general resources about this topic:
The text was updated successfully, but these errors were encountered: