One of the most frustrating things for a user is to receive a cryptic error message. This is closely followed by a developer receiving the same error message. Let’s say you’re using Fluent NHibernate and you get the error message “Sequence contains no elements”. Chances are, you’ve done something wrong. The problem is that you’re not going to figure out what unless you actually debug the source code. This particular error message is generated by the “Single” extension method. This code by design does the following:
- Returns the first element in the list
- OR Generates a cryptic error message
There’s no sensible way to use the function short of putting a try/catch/rethrow around every single invocation. You couldn’t even recode the function to be more helpful: there’s simply no useful information provided to it. Basically, there’s no way Single (or First, for that matter) can be used in high-quality code. SingleOrDefault and FirstOrDefault, on the other hand, have a way of dealing with failures that doesn’t involve cryptic errors messages (you can still throw an exception, and that can still be cryptic, but then it’s your fault, not Microsoft’s).
How else could we have designed this API? Well, we could try “First(Func<Exception> onError)”. This would be fine for First, because there’s only one failure mode: there’s no elements in the list. However, for Single there are two possible failure conditions: no elements and multiple elements. Do we add in two onError functions or do we create a Problem input? And then should we be using Func<Problem, Exception> or going for the general stable case of ProblemVisitor?
The answer is probably that the last case doesn’t come up: if you’ve got that complex a spectrum of failure conditions, you’ve probably provided the code with enough inputs to generate good error conditions. However, even in fairly general cases, sometimes you need to pass information to a function purely to improve the diagnostics. On the other hand, functions like First, Single and Parse (TryParse is okay) will never have good diagnostics are pretty much impossible to use in easy to debug code.