Skip to content
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

Closed
iam3yal opened this issue Mar 7, 2016 · 19 comments
Closed

Proposal: Declarative approach to exception handling #9539

iam3yal opened this issue Mar 7, 2016 · 19 comments

Comments

@iam3yal
Copy link

iam3yal commented Mar 7, 2016

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.

public T SomeIOoperation<T>() where T: class
{
    IO io = null;

    T result = default(T);

    try
    {
        io = new IO();

        result = io.DoSomething<T>();
    }
    catch(Exception ex) 
    {
        // Handle the exception
    }
    finally
    {
        io?.Dispose();
    }

    return result;
}

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.

private void ExceptionHandler(Exception ex) 
{
    // Handle any other exception
}

private void ExceptionHandler(IOException ex) 
{
    // Handle IO exception
}

public T SomeIOoperation<T>() where T: class
{
    IO io = null;

    try 
    {
        io = new IO();
        return io.DoSomething<T>();
    }
    catch ExceptionHandler // Points to a method group, single catch statement.
    finally io // Calls dispose implicitly. (Optional)

    return default(T);
}

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:

static class IOExceptionHandlers
{
    public static void ExceptionHandler(IOException ex) { }
    public static void ExceptionHandler(Exception ex) { }
}

public T SomeIOoperation<T>() where T: class
{
    IO io = null;

    try 
    {
        io = new IO();
        return io.DoSomething<T>();
    }
    catch IOExceptionHandlers.ExceptionHandler
    finally io

    return default(T);
}

What would the compiler generate?
The code that would be generated by the compiler is something like the following:

public T SomeIOoperation<T>() where T: class
{
    IO io = null;

    T result = default(T);

    try
    {
        io = new IO();

        result = io.DoSomething<T>();
    }
    catch(IOException ex)
    {
        ExceptionHandler(ex);
    }
    catch(Exception ex) 
    {
        ExceptionHandler(ex);
    }
    finally
    {
        io?.Dispose();
    }

    return result;
}

I'm not sure whether the call to ExceptionHandler is even necessary, maybe the compiler can inline this or something...

Advantages:

  1. It separates the task that the method needs to do from the responsibility of handling exceptions.
  2. It reduces the noise of exception handling when it's needed and so it greatly helps in terms of readability and making sense of the code.
  3. It is an option for people who wants something in between the using statement and the traditional try/catch block.
  4. It helps making exception handling handlers more reusable so it also helps in terms of maintainability.

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:

  1. The only information that the callee (the function that handles the exception) has is the exception, things like local variables won't be available to provide more information which can be useful.
  2. It's not possible to return out of the caller.
  3. It's not possible to use break or continue in the caller.
  4. It's not possible to rethrow meaning to use 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:

  1. C++
  2. D
@iam3yal iam3yal changed the title Why do we stick to the imperative approach of exception handling? Suggestion: Declarative approach to exception handling Mar 8, 2016
@iam3yal iam3yal changed the title Suggestion: Declarative approach to exception handling Proposal: Declarative approach to exception handling Mar 8, 2016
@HaloFour
Copy link

HaloFour commented Mar 8, 2016

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 (when) or patterns.

I don't particularly care for the scope guard syntax of languages like D, for the same reasons that I don't like defer (#8115). It becomes impossible to read what a method will do in the order in which it will be done since the deferred operations occur backwards. Scope guards up the ante in that you have to know which context under which the operation will occur. I think it's pretty telling that both Swift's and D's example code are incredibly contrived and do not demonstrate why the feature has real-world value.

You also imply automatic disposal of IDisposable types, but this seems quite inappropriate because the compiler shouldn't be making the determination as to when that type should be disposed, if ever. Why should the compiler assume that IO.DoSomething() isn't then assigning IO to some instance elsewhere that would be used later or by another class? What of the types where disposing is intentionally optional may perform actions that aren't always wanted? For example, StreamWriter is IDisposable and calling Dispose on it will close the underlying stream which is frequently enough not a desired behavior.

@whoisj
Copy link

whoisj commented Mar 8, 2016

The one use-case I could see here is not having to write repetitive catch clauses.

Too often I see this repeated ad nauseum try { .. } catch (Exception e) { Log(e); throw; } perhaps a way to add a handler at a higher level could make for less boiler plate? Would like obscure intent however.

@bondsbw
Copy link

bondsbw commented Mar 8, 2016

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'.");
}

@iam3yal
Copy link
Author

iam3yal commented Mar 8, 2016

@HaloFour

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 (when) or patterns.

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. :)

I don't particularly care for the scope guard syntax of languages like D, for the same reasons that I don't like defer (#8115). It becomes impossible to read what a method will do in the order in which it will be done since the deferred operations occur backwards. Scope guards up the ante in that you have to know which context under which the operation will occur. I think it's pretty telling that both Swift's and D's example code are incredibly contrived and do not demonstrate why the feature has real-world value.

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.

You also imply automatic disposal of IDisposable types, but this seems quite inappropriate because the compiler shouldn't be making the determination as to when that type should be disposed, if ever. Why should the compiler assume that IO.DoSomething() isn't then assigning IO to some instance elsewhere that would be used later or by another class? What of the types where disposing is intentionally optional may perform actions that aren't always wanted? For example, StreamWriter is IDisposable and calling Dispose on it will close the underlying stream which is frequently enough not a desired behavior.

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,
people just fall back to the ordinary try/catch block.

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.

@bondsbw
Copy link

bondsbw commented Mar 8, 2016

The only question I'd have (regarding my tweaks) is how to throw after the exception is handled. If you specify the behavior it in the try using clause (e.g. try using ExceptionLogger throw) you would only get one behavior or the other, but today's exception handlers can choose to throw in some conditions and not in others. The handler signature could be updated to return an enumeration that specifies the behavior instead of void:

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 when or patterns.

@HaloFour
Copy link

HaloFour commented Mar 8, 2016

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 break, no return, no continue. The best you could do is throw, but the CLR will not let you rethrow, so the stack trace will be clobbered. If your exception handler is doing so little that this functionality fits the bill then you're probably doing exception handling wrong anyway.

@iam3yal
Copy link
Author

iam3yal commented Mar 8, 2016

@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.

@HaloFour
Copy link

HaloFour commented Mar 8, 2016

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 rethrow). I also agree that long-winded exception handlers are a bad practice. I don't do either. My exception handlers are almost exclusively limited to throwing a new exception that better explains why something went wrong. Said exception handlers are typically completely unique and typically require access to the current scope. If I can do compensatory logic then I will, and that will be in its own method that is unrelated to an exception handler but can be called from one. SRP indeed, I intentionally don't mix the two. If I have nothing to add and nothing to do, I don't handle the exception. There's simply no reason to. Let downstream handle it.

@iam3yal
Copy link
Author

iam3yal commented Mar 9, 2016

@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.

@alrz
Copy link
Member

alrz commented Mar 9, 2016

Related: #4890

@dsaf
Copy link

dsaf commented Mar 9, 2016

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):

[SuppressesExceptionsReturnsDefault]
public T SomeIOoperation<T>() where T: class
{
//...
}

Java's throws also comes to mind.

@iam3yal
Copy link
Author

iam3yal commented Mar 9, 2016

@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.

Declarative is something like this (it's not about custom attribute but rather the form):

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.

@dsaf
Copy link

dsaf commented Mar 9, 2016

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 using:

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

@iam3yal
Copy link
Author

iam3yal commented Mar 9, 2016

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

@HaloFour
Copy link

HaloFour commented Mar 9, 2016

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 catch (Exception exception) => expression is likely to be considered.

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

@iam3yal
Copy link
Author

iam3yal commented Mar 9, 2016

@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.

@dsaf
Copy link

dsaf commented Mar 9, 2016

I am personally only using expression bodies on truly one-liner methods - sparingly.

@dsaf
Copy link

dsaf commented Mar 9, 2016

@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 if-else statements.

@HaloFour
Copy link

HaloFour commented Mar 9, 2016

@dsaf I know that with try blocks the curly braces are required because otherwise the compiler would not be able to determine which catch blocks are associated with which try blocks in the case of nested try blocks. As for why catch and finally also require curly braces, I'm not aware of any syntactic reasons other than perhaps consistency.

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

No branches or pull requests

8 participants