Replacing Throwing Exceptions with Notification in Validations
martinfowler.com
martinfowler.com
I've been hearing this line since the '90s. I've never heard a justification for it, or a rigorous explanation of what "expected" means.
Exceptions are a language construct, like variables, classes, and for loops. As with those other constructs, use them when it results in a program which makes sense. But that's a judgment you can only make on the program, not on the construct.
While I am all about respecting our predecessors and their good work (probably more so than most HN folk, though my respect rarely shows on HN itself; much of it is on topics not HN-related), I dislike that sort of fealty to their opinions. It shuts down thought and conversation. Whether exceptions should be "exceptional" should be something discussed carefully, not simply true by definition. Personally I'd observe that due to immense variances in exception semantics and performance in all the various runtimes and languages of the world there isn't a blanket statement you can make about whether that is true; for instance, C++ and Python have radically different criteria for what an "exception" should be, written into their nature, and just dogmatically repeating "exceptions should be exceptional" won't help you understand anything about the details of either language, or how they differ.
The usual justification I've heard is that it is superior for the flow of operation for expected cases to be clear from inspecting the code, and the use of exceptions breaks that up.
> or a rigorous explanation of what "expected" means.
This is a problem. I have my own construction of what it makes sense for "expected" to mean in this context [0], but its not clear to me that that's what others offering this rule necessarily do mean by "expected", it usually seems to be fuzzier (which is problematic.)
> Exceptions are a language construct, like variables, classes, and for loops. As with those other constructs, use them when it results in a program which makes sense. But that's a judgment you can only make on the program, not on the construct.
I generally agree, but I think that -- with the understanding of "expected" relating to the advertised constraints/expectations of the unit of code in question -- its a fairly good rule of thumb for when using exceptions results in code that makes sense.
result, err := fn1()
if err != nil {
return err
}
Or worse, the return is replaced with a "goto".As for the original article, JSON is machine to machine communication. Why do you need to report more than the first error? One of the programs talking is broken. What next, parser error recovery for bad JSON?
JSON is a data serialization format; it might be used for machine-generated data, but it can also be used to serialize raw user input and transport it to another node for validation. For the latter use case, multiple errors can be important.
I feel so smart right now because Martin Fowler validated (no pun intended!) my own thinking on this issue.
Both conceptually (a form validation error is not exceptional -- it is expected behavior that you want to handle within the flow of your program), and practically (throwing exceptions makes it harder to display multiple errors for the same field, so the user winds up playing "whack-a-mole" when there are a lot of different rules... e.g. "password must be at least 6 characters", "password must contain a letter and a number", etc.)
How would this pattern translate to Python, where exceptions are the norm and expected behavior?
note.addError("numberOfSeats", "number of seats must be positive");I did it this way because if someone calls the validation function and wants the method to fail immediately if something is wrong, it will. No need to check, it just does. But if you'd like to try validating and see if something messes up, you can!
[1]: https://github.com/callmeStriking/affirm-json (see src/validator.js or, heck, just run lib/valitest.js)
Expected behavior --> notifications. Unexpected behavior --> exceptions. Simple.
If you want to use some of the same validation code for user input validation and internal validation, I suppose you could modify the notation pattern to conditionally throw an exception when adding a new error depending on whether it's on the client or the server.
Or you reuse the validation code itself, but called from a wrapper that turns notifications into exceptions, rather than adding branches.
data Validation e a
= Collect e
| Value a
deriving Functor
instance Monoid e => Applicative (Validation e) where
pure a = Value a
Collect le <*> Collect re = Collect (le <> re)
Collect e <*> _ = Collect e
_ <*> Collect e = Collect e
Value f <*> Value a = Value (f a)
There is no monad instance for this which lets us scoot forward through a computation and and all of the errors at once.In other languages I would probably prefer to follow the approach in the article.
One criterion you could apply is that exceptions should only be thrown for violations of the code's published expectations that can not be statically prevented. Whether a validation failure would be "unexpected" then depends on whether the code is expressly validation code that expects unvalidated data (in which case validation failures would not be unanticipated) or processing code which "expects" good data and does something with good data, but throws an exception on bad data -- note that in the same workflow, at different levels, both models could be used, with different levels creating or swallowing exceptions depending on the expectations of the particular function.