5

Idea

I am thinking of using callbacks instead of throwing exceptions in C# / .NET.

Pros and Cons

The advantages are

  • no hidden goto like control flow of unchecked exceptions
  • much cleaner code, especially if more than one exception is involved
  • thrown exceptions are documented in the method signature and the caller is forced to think about handling exceptions but can easily pass an application-wide exception handler, an "UnhandledExceptionHandler", or null to them. So they are kind of like "soft" checked exceptions but more maintainable because exceptions can thrown afterwards by overloading the method or exceptions can be removed by no longer calling "handle" on the exception handler).
  • works also well for asynchronous calls
  • an exception handler can handle several exceptions that are thrown at different places
  • makes explicit which exceptions should be handled. Throwing exceptions the ordinary way could still be used for exceptions you don't want to be handled like "NotImplementedException".

The disadvantages are

  • not idiomatic to C# and .NET
  • throwing method has to interrupt the control flow by immediately returning a return value. This is difficult if the return type is a value type.
  • ? (see question below)

Question

I am probably missing a few critical disadvantages, because I am wondering why this is not used. What disadvantages have I missed?

Example:

Instead of

void ThrowingMethod() {
    throw new Exception();
}

and

void CatchingMethod() {
    try {
         ThrowingMethod();
    } catch(Exception e) {
         //handle exception
    }
}

I would do

void ThrowingMethod(ExceptionHandler exceptionHandler) {
    exceptionHandler.handle(new Exception());
}

void CatchingMethod() {
     ThrowingMethod(exception => */ handle exception */ );
}

with

delegate void ExceptionHandler(Exception exception);

defined somewhere and "handle(...)" being an extension method that checks for null, retrieves the stack traces and possibly throws an "UnhandledException" if there is no exception handler at all when the exception is thrown.


Example of throwing an exception in a method that did not throw an exception before

void UsedToNotThrowButNowThrowing() {
   UsedToNotThrowButNowThrowing(null);
}

//overloads existing method that did not throw to now throw
void UsedToNotThrowButNowThrowing(ExceptionHandler exceptionHandler) {
    //extension method "handle" throws an UnhandledException if the handler is null
    exceptionHandler.handle(exceptionHandler);
}

Example with methods that return values

TResult ThrowingMethod(ExceptionHandler<TResult> exceptionHandler) {
        //code before exception
        return exceptionHandler.handle(new Exception()); //return to interrupt execution
        //code after exception
    }

TResult CatchingMethod() {
     return ThrowingMethod(exception => */ handle exception and return value */ );
}

with

delegate TResult ExceptionHandler<TResult>(Exception exception);
  • My first remark is that you are missing a construct to catch exceptions that are caught lower down the stack. For example you are doing processing of a file, using a lot of methods. Then at some point you have a read error (faulty hardware, whatever). In the `Exception` case, you can throw an `IOError` and catch it way down the line, giving the user a general error, wind back the stack, release resource and be done. – Bart Friederichs Jun 27 '13 at 15:31
  • 5
    This question appears to be off-topic because it is about general programming paradigms, not a specific programming problem. Try programmers.stackexchange.com – Bart Friederichs Jun 27 '13 at 15:36
  • Please don't pass null... You will end up writing `if (h==null) { do something; } else { safe to call h.something() }` in the callee. – Ali Jun 28 '13 at 10:34

3 Answers3

0

For one thing, you'll have the overhead of needing these handlers passed to pretty much every method in your application. That's a very heavyweight dependency and a do-or-die decision to take prior to building the app.

Secondly, there's the issue of handling system-thrown exceptions and other exceptions from third-party assemblies.

Thirdly, an exception is meant to halt the execution of the program at the point it is thrown, since it really is "an exception" and not just an error that can be handled, allowing execution to continue.

mungflesh
  • 776
  • 11
  • 22
  • Very good points. Thank you! You could still use try...catch for existing exceptions and throw an "UnhandledException" if there is no handler, but I'll definitely have to investigate the performance impact. It could very well be a deal breaker. –  Jun 28 '13 at 06:06
0

Scalability.

As @mungflesh correctly points out you have to pass these handlers around. My first concern wouldn't be the overhead but scalability: It influences the method signatures. It would probably lead to the same scalability issue as we have in Java with checked exceptions (I don't know about C#, I only do C++ and some Java).

Imagine a call stack with a depth of 50 calls (there is nothing extreme about it, IMO). One day a change comes up and one of the callees deep down the chain that did not throw becomes a method that can now throw an exception. If it is an unchecked exception you only have to change the code at the top level to deal with the new error. If it is a checked exception or you apply your idea, you have to change ALL the involved method signatures through the call chain. Don't forget that signature changes propagate: You change the signature of those methods, you have to change your code everywhere else where those methods are called, possibly generating more signature changes. In short, scales poorly. :(


Here is some pseudo-code showing how I mean it. With unchecked exceptions you handle a change in the callstack of depth 50 the following way:

f1() {
  try {    // <-- This try-catch block is the only change you have to make
    f2();  
  }
  catch(...) {
    // do something with the error
  }
}

f2() { // None of the f2(), f3(), ..., f49() has to be changed
  f3();
}

...

f49() {
  f50();
}

f50() {
  throw SomeNewException; // it was not here before
}

Dealing with the same change with your approach:

f1() {
  ExceptionHandler h;
  f2(h);
}

f2(ExceptionHandler h) { // Signature change
  f3(h); // Calling site change
}

...

f49(ExceptionHandler h) { // Signature change
  f50(h); // Calling site change
}

f50(ExceptionHandler h) {
  h.SomeNewException(); // it was not here before
}

All of the methods involved (f2...f49) now have a new signature AND the calling sites have to be updated as well (e.g. f2() becomes f2(h), etc). Note that f2...f49 shouldn't even know about this change, yet, both their signature and calling sites have to be changed.


To put it in another way: all the intermediate calls now have to deal with the error handlers even though it is a detail they should not even know about. With unchecked exceptions, these details can be hidden.


Unchecked exceptions are indeed "hidden goto like control flow" but at least they scale well. No doubt, can lead to an unmaintainable mess quickly...

+1 though, an interesting idea.

Ali
  • 56,466
  • 29
  • 168
  • 265
  • Thanks! Checked exceptions are one of the reason why I don't like Java that much and I am glad that C# has unchecked exceptions. I think this is something in between, or both. To add a new exception to an existing method, you could overload the method with the new exception handler parameter which would be used by the existing method. Since there won't be any handlers of that new exception, an "UnhandledException" will be thrown as with unchecked exceptions. Or, you do have the additional effort as with checked exceptions if you want to propagate the new exception up the method signatures. –  Jun 28 '13 at 06:24
  • @ExercitusVir "To add a new exception to an existing method..." No, no, no. Your missing my point. In the example I write in my answer, a method that did **not** throw became a method that now can throw an exception. Please read that example again and you will see that such code changes can be a real pain. You don't have that with unchecked exceptions because they are sort of hidden (which can also lead to messy code). I don't like Java either but it has unchecked exceptions as well. I guess at some point they realized the checked exceptions are not that good after all. – Ali Jun 28 '13 at 07:01
  • I think I did understand it the way you describe it. The method that did not throw would itself call the new method with the added delegate parameter which now "throws" by passing null or "NoEventHandler" as the EventHandler for that new exception. So you basically move the code of the method that did not throw into the new method that throws and call that method from the method that did not throw. Do you know what I mean, or am I still not getting it something? –  Jun 28 '13 at 07:32
  • @ExercitusVir Maybe I don't understand you... or I failed to express myself. I have added an example showing the scalability issue. I hope it's now clear how I mean it and shows my understanding of your idea. Please correct me if I misunderstood something. – Ali Jun 28 '13 at 09:11
  • @Ali: Java has always had both checked and unchecked exceptions. For the most part, the choice of which type to throw depends on the source of the error. Unchecked exceptions are intended to be triggered entirely by internal wonkiness (things that should never really happen, like null pointers, invalid args, out-of-bounds array indexing, etc), while things outside the app's control (network, file system, etc) will generally throw checked exceptions, since even a moderately robust app should be prepared to deal with that stuff at some point. – cHao Jun 28 '13 at 09:29
  • @Ali: Yes, you could propagate the exception handler parameter as in your example, but you don't have to. I've added an example on how to do that to the question. So the developer has the choice what to do. –  Jun 28 '13 at 09:54
  • @cHao: Yes, Java has unchecked exceptions, but are still forced to handle checked exceptions that you know cannot be thrown. For example, in Java you are forced to handle a FileNotFoundException even if you have checked that the file exists. I think the caller and not the callee should decide what to handle (advantage of unchecked exceptions) without having to dig through the documentation of the entire call tree (advantage of checked exceptions). –  Jun 28 '13 at 10:13
  • @ExercitusVir I am afraid I still don't get it. Please give me an example in your question with a callstack of depth 50, where you don't have to propagate the exception handler in case of the 50. method becoming suddenly a throwing method. What you currently show as an example that corresponds to depth 1. – Ali Jun 28 '13 at 10:23
  • @Ali: Please see the example that I added to the question. Your "f50" corresponds to my "UsedToNotThrowButNowThrowing". f1 to f49 can remain unchanged because f49 still calls the overloaded f50() which now calls the f50(ExceptionHandler h) by passing null as the ExceptionHandler. –  Jun 28 '13 at 10:35
  • @ExercitusVir Please give me some time, I am busy at the moment but I promise I will get back to you. – Ali Jun 28 '13 at 10:38
  • @ExercitusVir: Of course you're forced to handle it, or let it propagate *and tell the world you're doing so*. You can't reliably avoid throwing a `FileNotFoundException`, even by checking for existence beforehand, and the ability to now throw one means a new way to crash over stuff outside of the code's control. – cHao Jun 28 '13 at 11:56
  • @ExercitusVir OK, In my example `f1` knows that something, somewhere may go wrong and also `f1` knows how to handle the exception. In other words, it is `f1` how has the try-catch block or creates the `ExceptionHandler`. In your example it's `f49` so the scalability issue is not visible yet. Usually, top level methods know that something may go wrong and how to handle it, all the intermediate call don't bother. So please create an example accordingly, where `f1` creates the `ExceptionHandler` and `f50` becomes a throwing method. – Ali Jun 28 '13 at 13:24
  • As for passing a `null`, I think it's a bad idea, partly because you always have to check for null later on and act accordingly in the callees. You will either trash your code with `if-else` or throw NullPointerExceptions (whatever the C# equivalent is). It's also clear to me how to handle the situation where you have a `null` exception handler and you should raise an exception. Do you crash with a NullPointerException? – Ali Jun 28 '13 at 13:27
  • @Ali Ah, now I understand what you mean. To catch any unhandled exception in the call tree inside f1 without passing down the exception handler f1 would have to remain unchanged, because f50 will throw an "UnhandledException" if null is passed as an exception handler, which f1 will still catch with try...catch. So yes, this still requires using a try...catch at the top. –  Jun 28 '13 at 17:45
  • @Ali (continued because too long): So you basically have to mix both approaches, which isn't ideal, but I think acceptable considering that there shouldn't be many methods that catch all unhandled exceptions. So yes, there are definitely issues with this approach. P.S.: The "handle" extension method should automatically checks for null and throw an "UnhandledException", so the caller of "handle" does not have to check for null. –  Jun 28 '13 at 17:45
  • @ExercitusVir OK, I accept your approach for handling `null`s. I still don't see how you manage the situation (without propagating the exception handler as I show in my answer) when f1 must create the ExceptionHandler (because only he knows/cares) and f50 becomes the throwing method (previously was non-throwing) deep down the call chain. I ran into very similar situations with checked exceptions when I was working with large Java projects. As I see it, your approach has the exact same scalability issue. – Ali Jun 28 '13 at 18:02
  • @cHao I stand corrected: Java has always had unchecked exceptions. However, I still think checked exceptions are a bad idea, exactly for the scalability issue I am showing in my answer. But let's not start a debate on [checked vs. unchecked exceptions](http://stackoverflow.com/q/6115896/341970) :) – Ali Jun 28 '13 at 20:17
  • 1
    @Ali: I'm not a huge fan of checked exceptions; i'm mostly playing devil's advocate. But realize that *all* exceptions, checked or not, can cause scalability issues. Each one that bubbles up is a potential app killer. And now you've added a new one -- and no one's the wiser, til some odd circumstance triggers it. Yeah, it'd be hard to change a function 50 levels deep to make it throw a checked exception. But it *should* be. Potential exceptions are a part of the interface, and the interface should be fleshed out sometime before you put 50 layers of calls around it. :) – cHao Jun 28 '13 at 22:13
  • @Ali f1 still catches it without propagating the exception handler because it's still using a try...catch and f50 throws an "UnhandledException" (which could wrap the actual exception) using the throw keyword (because the exception handler is null). It works exactly like unchecked exceptions in this case. –  Jun 28 '13 at 23:28
  • @cHao Let's just wrap this discussion up by saying that exception handling can become really tricky, no matter how you do it. As for the checked exceptions, even though I don't like them, I must say there is some value in making explicit that the method can throw. Anyways, thanks for your feedback on the subject. – Ali Jun 29 '13 at 08:57
  • @ExercitusVir Well, if you resort to the unchecked exception case in the example I show then you see what the scalability issue is with your proposed method. If you think my example is artificial, well, it isn't, I ran into the same problem several times but with checked exceptions when I was working on a huge Java project. Anyways, as I said before, I upvoted your question, an interesting idea. – Ali Jun 29 '13 at 09:02
  • @Ali Thanks for your valuable feedback! –  Jun 30 '13 at 22:38
0

If I get it right, if there are two possible exceptions in a method, that method need to take in two different parameters. In order to simulate the checked exceptions and make the caller be informed about the possible exceptions, you have to pass different handlers for different kinds of possible exceptions. Therefore in polymorphic situations, when you are defining an interface or an abstract class, you force the possible exceptions to the unwritten code, so the concrete implementations are not allowed to generate new types of exceptions.

As an example think you are implementing the Stream class and FileStream concrete class. You have to pass in an exception handler for the file not found exception which is bad because it forces the MemoryStream to take in the file not found exception handler, or on the other hand, you are not allowed to generate the file not found exception in FileStream, because signature does not allow it.

mehdi.loa
  • 579
  • 1
  • 5
  • 23