Instead of letting code throw exceptions around, pass an error handler to it that it can consult in the event of a failure.
This is a simple form of the StrategyPattern.
ExceptionHandlingChallenge entry:
Following the example in ExceptionHandlingChallenge, Python code might be:
def handle_database_failure():
redirect("databasefailure") # assuming this jumps somewhere (bad idea, but same as in the example)
def handle_email_failure():
UserDatabase.delete(email, handle_database_failure)
redirect("mailfailure")
User'Database.add(user, handle_database_failure)
mailPassword(email, password, handle_email_failure)
redirect("success")
Note how the exception that could arise from the UserDatabase.delete is handled properly. You might say "well, we'd just spot that and add another try/catch block in there". However, the point is that when passing an error handler, the handling is enforced by the delete method as it requires the handler in a parameter. I.e. if it doesn't get one, we find out straight away that the code is broken.
Pros:
- Nudges you towards OnceAndOnlyOnce. The ReturnBoolean? and Try/Catch versions might tempt programmers to repeat the handling code with every use.
- More unit-testable. Because the error handler only handles an error, we can simply call it in a unit test to test it. Contrast this with testing a try/catch block which contains very specific code in the try-block. To test the error handling in a Try/catch structure, we have to make sure that code fails, which requires knowledge of intricate details of how that code can fail. To test an error handler, we just call it. No actual error condition needs to be set up.
- The only one that can really extend to large system (I'm only a student, but I think I'm right). The exception handling and if examples put the error handling code relatively near the calling code, which means that the code will be large (you need to repeat the error handlers everywhere), and that the error handlers cannot be standardized across the whole program. The invisible exception handlers, though, has only global handlers, so your classes have to be closely related to each other, and they violate the OneResponsibilityRule. Passing an error handler lets you have a standard error reporting mechanism, in addition to handling of errors in special cases (especially if you use stacks of error handlers, which gives you more flexibility). - please clarify these, I've split some of them out as what I think you're saying in below.
- Works well as system size grows. As a system grows, in general, there will be an increase in the number of layers of code between code that might raising an error, and code that handles that error. In any of these layers, other exceptions may be raised. If the exception handler is passed to the code that can raise the exception, there can be no confusion about which handler handles which error.
- Allows easy 'stacking' of error handlers, providing more flexibility.
- Does not require any extra support from the language in the form of reserved keywords such as 'try' or 'catch'. Therefore, namespaces are not so polluted, and this technique is usable even in languages without any exception support.
- Works well asynchronously - it is still possible deal with the error correctly, even if the original call was instigated from a different thread of execution which has already terminated, or perhaps a different machine to which the network connection has been lost. Node.js is an example.
- Explicit responsibility - either the calling code passes in its own handler, (taking responsibility itself), or it must pass on the responsibility further up. Failure modes therefore become an explicit part of the API without need for special keywords and method/function signature decoration such as 'throws XYZException'.
- Proper error handling is better enforced by the API on the client, i.e. if the called code changes and new exceptions can be raised, all client code are forced to take that into account, since their API must change. Thus, code can be easier to reason about than code which may raise unexpected exceptions due to future changes.
- Allows for localised exception handling. At the point where the handler is called it can be given access to everything in scope, and deal with it there and then, before continuing.
- If necessary, the handler can be substituted for a proxy handler which does two things:- (1) calls out, perhaps via a ChainOfResponsibility, to real handlers, (2) runs a test to make sure that the exception really has been handled, i.e. checks to make sure the error condition no longer exists, and if not handled, creates a 'harder' error. This can provide a level of guarantee that errors will be handled correctly and not simply be ignored (i.e. by do-nothing handlers).
Cons:
- Possibly not the SimplestThingThatCouldPossiblyWork. It involves writing an extra pair of classes (FailureHandler and MockFailureHandler for testing). -(using classes means the failure handling code has been extracted and thus can be more easily reused and composed. Please note FearOfAddingClasses)
- Uses AnonymousInnerClasses (some -(who?) consider this reaching too far into your BagOfTricks?). -(not necessary that these classes are anonymous, as described above)
- Is ugly, by some standards. (whose standards?)
- Does it hide or obscure the failure paths in the mainline code? -(arguably it makes failure points explicit, and thus easier to reason about)
- Don't you still need exceptions or return codes to prevent the 'mailPassword' and 'redirect("success");' calls from occurring, should errors occur? Or does your inner class set some member variable that is checked by 'if' statements that still need to be added? -(probably do still need exceptions or some other mechanism for safe termination in the general case)
- Still possible to ignore exception handling altogether. -(unless using handler checks as described above)
Further notes:
- If we have created any resources that will need destroying in the case of an error, we can't guarantee destruction as is possible with a 'finally' block with a plain error handler. What we can do instead however is wrap the error handler in a "cleanup" handler. When the error occurs, the cleanup handler handles it by passing it on to the original handler, and when that is done, cleaning up the resources. Thus we can preserve the guarantees, and keep the responsibility for cleaning up with the code creating the resources.
Cf. ResumableException, CommonLispConditionSystem
CategoryException