I was a bit surprised at what happens when you combine spec with pre/post conditions. I use this sparingly at the boundaries of API calls/object coercions. I expected failed coercions to give useful information, but it doesn't. Is there a better way to guard structured at the boundaries?
I ended up writing the following util function that does what I was hoping that pre/post conditions conditions would do, but I feel like I am doing something "wrong" by not just using the built-in functions. Why do they behave like that, and why is what I am doing not a good idea?
(defn defend-spec
[spec candidate]
(if-let [explained (s/explain-data spec candidate)]
(throw (ex-info "Object failed to conform to spec" explained))
candidate))pre/post use assertions which are Error in the Throwable hierarchy and really aren't expected to be caught.
ex-info produces an Exception which is reasonable to catch.
I don't see pre/post used much in the wild, partly because assertions are generally "stop the world" (and a lot of people think they should be turned off in production -- I disagree), and partly because they don't produce very good information.
Some people will do this instead: (assert (s/valid? spec candidate) (s/explain-str spec candidate)) -- that will still be an Error but at least the message will now be a useful(?) explanation of the failure.
Note that some people think it's perfectly fine to catch Throwable but you'll see a lot of advice saying don't do that, only catch Exception.
Why would they be turned off? I probably want the world to stop if data coming in outside my system is wrong. I can't necessarily predict what it will do in my database
Well, exactly... But some folks think assertions are for dev/test only 🤷🏻♂️
Performance is one reason someone might want to turn them off