the recommendation is to prefer returning a result object than
throwing an exception
This is bad advice IMHO, because they should be used in different circumstances. Using a result object when an exception is most appropriate is bad.
If ValidateUserInput
is not able to perform its task because (for example) the process is out of memory, then throwing an exception is the right thing to do. Returning a value that indicates that the input is either valid or invalid would obviously be wrong since we don't know that. Calling "out of memory" a validation error would be misleading, since it is not an error in the input. Creating three cases: valid, invalid and out-of-memory would be bizarre.
Exceptions should be used to signal when a method is not able to complete its task. Result objects is for when a method is able to complete its task, but the result can falls into different cases.
For example a Search()
method might return an option type with a case when a match is found and another if no match is found. But it should throw an exception if the database is disconnected, there was a timeout or anything else which means the search couldn't be performed.
So what about the argument in the linked blog post, which say you should always use result-objects instead of exceptions?
The post suffers IMHO from a a symptom I would call language envy. It tries to shoehorn patterns from a different language without considering the different context of C#. Typically you end up with worst of both languages. Some points to consider:
- .net still have exceptions. Whether you like it or not, the framework and 3rd party libraries will still throw exceptions.
- .net exception are unchecked which means you don't know at compile time exactly which exceptions a method may throw.
This means you can't really get the purported advantages by turning all errors into a result-object. For example:
Methods which throws exceptions are not "honest". You can't recognize
if a method is expected to fail or not, by looking at its signature.
If you think this is an issue, it is not going away. The framework and 3'rd party libraries still don't advertise what exceptions they may throw, which means your code could also pass on an unadvertised exception.
If you consider this a problem, consider using a language like Java instead, which does have checked exceptions - or a language without exceptions.
Exception handling adds a lot of boiler plate code.
No more that encoding errors in result values! Result values require boilerplate to wrap, unwrap and to pass up the up the call stack - something which happens automatically (no boilerplate) for exceptions.
In a language like Rust with uses result-objects throughout, it was quickly realized how much boilerplate it required, so a shortcut syntax (first the try
macro, then the ?
operator) was introduced to alleviate that. Haskell has the do-syntax. C# does not have anything similar, and doesn't have macros, so you have to suffer the boilerplate.
The article ends up rewriting everything into non-idiomatic C# with a chain of OnSuccess
calls which end up being much more complex than the initial code it was supposed to simplify! (The article cheats a bit by have some needless redundancy in the "before" code which is magically gone in the "after" example.)
More importantly, the initial problem which the code attempted to solve, the transaction rollback, could be more elegantly solved in an idiomatic way by using a using
-block. The use of RollbackLastTransaction
seem like is could be vulnerable to race conditions.
When exceptions are used to control the flow, it has a "goto" semantic, where you can jump to specific line of code.
This is a red herring because it is recognized as an anti-pattern in C# to use exceptions for control flow. That is not their purpose. But in any case, comparing exceptions to GOTO is a misunderstanding of why GOTO's are considered bad in the first place. If an exception is like a GOTO, then so is a return
.
Bottom line: Choose the language which you prefer or which is most appropriate for the task. Then understand the design principles and idioms of the chosen language, and use them to your advantage instead of fighting them,