(s/or :success string?
:error ::error)(s/error <success-spec> <error-spec>)I tend to use spec exclusively at the boundaries. Think serialization. Sometimes, I have a producer which can't handle the error, so it must be serialized and exchanged to the consumer.
So say the producer couldn't produce the value, for some reason. In some cases, that's a bug, and I throw an exception, and fail the producer itself. But sometimes, its an expected possibility, in which case, tbe value is an error. And the producer must communicate that out.
Unfortunatly, exceptions are not friendly to serialization or spec. So I need to roll my own error structure, serialize that, and spec it.
For our REST APIs, we have middleware that catches exceptions, logs them, and returns an error response to the caller.
If we have a specific handler that can fail in an expected way, we write that into the logic and respond to the caller with an error response – much like the middleware does.
With just a small tweak on how the middleware works, we could easily replace the handler error logic with throw of ex-info (with appropriate data) and probably simplify our code.
Which is to say: I agree with Alex that exceptions are the idiomatic way to go in Clojure and ex-info / ex-data (and now ex-message and ex-cause) provide all the hooks you need for conveying error information along with those exceptions.
Sean Corfield -- (970) FOR-SEAN -- (904) 302-SEAN
An Architect's View -- http://corfield.org/
"If you're not annoying somebody, you're not really alive."
-- Margaret Atwood
This is my issue. I operate in a distributed environment. If I produce a set of data, but one field failed to compute properly, maybe a downstream system was down, maybe some information I was given to compute was corrupted, or missing, etc. And say this producing service has no user facing component, failing it is not logical. So I need to publish the partial result, and the error field should indicate an error. In my case it publishes a document entry in a nosql datastore, and events about it.
Now, some other system will eventually consume that document, to display it to the user. When it does, it must appropriately handle the fact that some fields were in error.
My documents are fully specced. So that consuming services can easily know their shapes and structure, so they can be sure to support them fully.
In such scenario, exceptions aren't useful, but only because Java exceptions are crap at serialization. So I need to do the same thing you are, marshal my exception into an error and serialize that into my document. Then I spec the field appropriately.
Now, I feel a lot of people using Spec would have such a use case, as its a logical tool to model data at your boundaries, and so I felt it might make sense to offer a spec macro for it.
I would likely only spec the status 200 OK responses. We use 400-series status values when we send back an error. You might consider that to be the “exception” of the HTTP world 😊
We actually do have a documented format for 400-series responses but pretty much any part can be omitted so callers might occasionally not be able to ascertain a reason beyond “it failed”…
Sean Corfield -- (970) FOR-SEAN -- (904) 302-SEAN
An Architect's View -- http://corfield.org/
"If you're not annoying somebody, you're not really alive."
-- Margaret Atwood
For any library – for any function – there are always two classes of unhappy path:
The former should not use exceptions. The library/function should signal the error in a documented way through its return value. Calling code should check the return value to see if the library/function failed in one of the expected, known, documented ways it is known to be possible to fail in, and respond accordingly.
The latter can (and should) use exceptions. An exception says “I got into a state I can’t handle because I wasn’t expecting to get there!” and maybe the caller can handle that and maybe it can’t. Library/function authors can help callers here by:
Does Java (and its standard library) overuse exceptions? Yes, absolutely. It throws exceptions for all sorts of completely predictable failure modes. We don’t need (or want) to be Java.
Clojure provides perfectly good features to support both the expected and the unexpected failure modes and, in particular, provides an excellent way to convey information about the point of failure even when our code doesn’t know how to recover.
As Alex says, there may be value in providing a spec in your library for the sort of ex-data you provide around exceptions. You’ll already be in “regular Clojure land” as far as functions that return values that may indicate success or expected, known failure modes.
Sean Corfield -- (970) FOR-SEAN -- (904) 302-SEAN
An Architect's View -- http://corfield.org/
"If you're not annoying somebody, you're not really alive."
-- Margaret Atwood
Given that the examples you provided are from typed languages and Clojure isn’t typed, what would satisfy you here? A “recommendation” from Cognitect that such success+result/failure+error-info situations use certain specific keys in a hash map for the overall return value? A new clojure.<namespace> that includes a spec for such a map and helpers for the ok/error cases?
I’m genuinely curious as to what you (and other folks) would like to see added to Clojure to satisfy this “need”.
First off, a nitpick: Clojure doesn’t have a standard library. There’s “the namespaces that ship in the org.clojure/clojure dependency” and then there’s “the various Contrib libraries”. The Clojure/core folks control the former but could only make recommendations regarding the latter (although, if a core change added a “standard” way to report s+r/f+e-i, then maybe Contrib authors would migrate to that – but see caveat below).
Second, as I indicated in my previous response, I think that code that throws exceptions for a perfectly predictable error situation is wrong – in most cases. Clojure.core/read is an interesting example because, by default, if it hits EOF, it throws an exception but it allows you to override that behavior and, instead, return a value at end of file. Normally, I’d say that default behavior is wrong: EOF is a predictable failure mode – but this is a case where it is most convenient to have read return the value that is read and there is no universal value it can return to indicate EOF since it can read and return any valid Clojure value (including nil or any namespace-qualified keyword/symbol etc). This is a case where the simplest possible API is to have read always return (just) the value it read on the happy path but to throw an exception if EOF is encountered during reading. It’s simpler because Clojure is a dynamic language without types like Either, Result, etc. Your choices are either force all users of read to deal with s+r/f+e-I map values and decoding or to force all uses of read to deal with exceptions.
I’m curious as to what you’d have clojure.core/read do here? (and feel free to point me at other Clojure functions that throw exceptions where you think returning s+r/f+e-i would be “better” – happy to look at several different cases!)
Sean Corfield -- (970) FOR-SEAN -- (904) 302-SEAN
An Architect's View -- http://corfield.org/
"If you're not annoying somebody, you're not really alive."
-- Margaret Atwood
I'm proposing we add a spec for it, which would conform to success or error.
Something like this, perhaps?
user=> (s/def ::result (s/and (s/keys ::req [(or ::ok ::error)]) #(not (and (contains? % ::ok) (contains? % ::error)))))
:user/result
user=> (s/valid? ::result {::ok 1})
true
user=> (s/valid? ::result {::error "bad!"})
true
user=> (s/valid? ::result {::error "bad!" ::ok "wat?"})
false
And, presumably, some helper functions to construct an “ok” or “error” result, and maybe ask if a result is “ok?” or “error?”, and maybe some convenient accessors? Or just ::ok and ::error directly?
Sean Corfield -- (970) FOR-SEAN -- (904) 302-SEAN
An Architect's View -- http://corfield.org/
"If you're not annoying somebody, you're not really alive."
-- Margaret Atwood
(defmacro errorable
[success-spec error-spec]
`(s/or :success ~success-spec
:error ~error-spec))
(s/def ::result (errorable string? #{:authentication-failure :missing-input})
(s/conform ::result "Here is a successful and valid result.")
-> [:success "Here is a successful and valid result."]
(s/conform ::result :missing-input)
-> [:error :missing-input]
Now we have a standard spec for something that is either in success or in error. And it conforms to standard representations, a vector pair, with first being either the :success keyword or the :error keyword.
You can add convenience function to this if you want, like:
(defmacro if-error
[spec value do-when-success do-when-error]
...)
(if-error ::result result
(display result)
(display "An error occurred, please try again.")
Given that the examples you provided are from typed languages and Clojure isn’t typed, what would satisfy you here?
First off, a nitpick: Clojure doesn’t have a standard library.
there are a few ways to deal with this
Flow reminds me a bit of a project I started in early 2015 and decided to sunset in late 2016: https://github.com/seancorfield/engine
We actually used Engine at work for a while but decided the resulting code was both harder to read and not really very idiomatic. I’ll be interested to hear how people find Flow in production – it’s a lot more focused and simpler than Engine (which is definitely a good thing! 😊 ).
Sean Corfield -- (970) FOR-SEAN -- (904) 302-SEAN
An Architect's View -- http://corfield.org/
"If you're not annoying somebody, you're not really alive."
-- Margaret Atwood
Alex, I’m curious, should this https://github.com/dawcs/flow/blob/master/src/dawcs/flow.clj#L53 use *exception-base-class* rather than Throwable directly?
It looks very interesting and elegant – I’ll probably give this a test drive next week!
Sean Corfield -- (970) FOR-SEAN -- (904) 302-SEAN
An Architect's View -- http://corfield.org/
"If you're not annoying somebody, you're not really alive."
-- Margaret Atwood
Ah, that makes much more sense. Got it!
Sean Corfield -- (970) FOR-SEAN -- (904) 302-SEAN
An Architect's View -- http://corfield.org/
"If you're not annoying somebody, you're not really alive."
-- Margaret Atwood