Exception handling library for clojure(+manifold)
We want to be able to express/handle exceptions via ex-info
in a
more formalised way.
We want to be able to support java like exceptions hierarchies, without having to define/extend classes.
We want the shape of our custom ex-info
s to be consistent.
We want to have a default/generic categorization of ex-info
s
We don't want to differ from the original try
/catch
/finally
semantics.
We want to have the same mechanism for manifold.deferred/catch
,
CompletableFuture
, optional via separate deps.
We want to minimize any performance penalty it might have.
We don't want to emit catch Throwable
, stay as close as possible
to what the user would write by hand.
This is an exception library, drop in replacement for
try
/catch
/finally
, that adds support for ex-info
/ex-data
with a custom clojure hierarchy that allows to express exceptions
relations. It also comes with manifold support.
So we have exoscale.ex/try+
, which supports vanilla catch
/finally
clauses.
If you specify a catch
clause with a keyword as first argument
things get interesting. We assume you always put an
:exoscale.ex/type
or :type
key in the ex-infos you
generate, and will match its value to the value of the key in the
catch
clause.
Essentially catch
takes this form:
(catch :something m
;; where m is a binding to the ex-data (you can destructure at that level as well)
)
So you can do things like that.
(require '[exoscale.ex :as ex])
(ex/try+
(throw (ex-info "Argh" {::ex/type ::bar :foo "a foo"}))
(catch ::foo data
(prn :got-ex-data data))
(catch ::bar {:as data :keys [foo]}
;; in that case it would hit this one
(prn :got-ex-data-again foo))
(catch ExceptionInfo e
;; this would match an ex-info that didn't get a hit with catch-ex-info)
(catch Exception e (prn :boring))
(finally (prn :boring-too)))
We leverage a clojure hierarchy so you can essentially create exceptions relations/extending without having to mess with Java classes directly.
;; so bar is a foo
(ex/derive ::bar ::foo)
(ex/try+
(throw (ex-info "I am a bar" {::ex/type ::bar})
(catch ::foo d
(prn "got a foo with data" d)
(prn "Original exception instance is " (-> d meta ::ex/exception))))
We have exoscale.ex.manifold/catch
that matches the semantics
of a catch
block in try+
but with a more manifold like feel.
(require '[exoscale.ex.manifold :as mx])
(require '[manifold.deferred :as d])
(-> (d/error-deferred (ex-info "boom" {::ex/type :bar}))
(mx/catch :bar (fn [data] (prn "ex-data is: " data)))
(d/catch (fn [ex] "... regular manifold handling here")))
You can specify a clojure.spec for the ex-data
via the multimethod at
:exoscale.ex/ex-data-spec
or via the sugar fn provided
exoscale.ex/set-ex-data-spec!
:
(ex/set-ex-data-spec! ::foo (s/keys :req [...] :opt [...]}))]}))`
By default this is enforced via clojure.spec/assert
, meaning unless
you toggled it "on" explicitely, it will be off.
You can change this behavior and set the validator to something else like a log statement:
(ex/set-validator!
(fn [data]
(when-not (s/valid? ::ex/ex-data)
(log/warnf "ex-data caught doesn't match the spec for it's :type : %s" data))))
It is strongly discouraged to make this validation breaking the flow of execution in production as this would only show up at "catch time".
You can also get the full exception instance via the metadata on the
ex-data we extract, it's under the :exoscale.ex/exception
key.
If you are within a try+
block you can also access it directly via
&ex
We suggest you also either use one of these as ::ex/type
or derive your
own with these.
Within the namespace :exoscale.ex
:
category | retry | fix |
---|---|---|
:unavailable | yes | make sure callee healthy |
:interrupted | yes | stop interrupting |
:incorrect | no | fix caller bug |
:forbidden | no | fix caller creds |
:unsupported | no | fix caller verb |
:not-found | no | fix caller noun |
:conflict | no | coordinate with callee |
:fault | no | fix callee bug |
:busy | yes | backoff and retry |
This is very much inspired by cognitect-labs/anomalies.
We have simple wrappers to create ex-info based on this:
ex/ex-unavailable
, ex/ex-interrupted
and so on. Their signature is
identical to ex-info
otherwise. An ex-http/response->ex-info
utility function that maps HTTP status codes onto this taxonomy is available in the ex.http
namespace.
Specify a :exoscale.ex/type
key always
The type key should either be one of our base type or a descendent. Descendents should be avoided when possible.
If it's a rethrow or comes from another exception pass the original
exception as cause
(3rd arg of ex-info
)
Have logging in mind when you create them. It's easier to pull predefined set of values from ELK or aggregate than searching on a string message.
ex-info
s should contain enough info but not too much (don't dump a
system/env map on its ex-data).
Preferably select the data you want to show in your ex-data instead of removing what you want to hide. If for some reason secrets end up in the upstream data source at least there's is no risk of leaking them with the exception that way.
If you use more than once the same type
you might want to spec it
Avoid returning raw values in error-deferreds, return properly
formated ex-infos
(d/error-deferred ::foo) vs (d/error-deferred (ex-info ".." {...}))
Do not leak data that is meant to emulate a usage context (cloudstack error codes, http status codes). That should be handled by a middleware at the edge.
We have a few helpers
exoscale.ex/ex-info
.
;; shortest, just a msg and {::ex/type ::incorrect}
(ex/ex-info "Oh no" ::incorrect)
;; same with some ex-data
(ex/ex-info "Oh no" ::incorrect {:foo :bar})
;; same with cause
(ex/ex-info "Oh no" ::my-error {:foo :bar} cause)
;; including derivation
(ex/ex-info "Oh no" [::foo [::incorrect ::sentry-loggable]])
(ex/ex-info "Oh no" [::foo [::incorrect ::sentry-loggable]] {...})
ex/ex-unavailable
,ex/ex-interrupted
, etc
They are just shortcuts to ex-info
with preset :type matching our
base type table and built in validation.
(throw (ex/ex-unavailable "Cannot reach foo" {:because :this}))
for testing you can use in exoscale.ex.test
thrown-ex-info-type?
similar to thrown?
in clojure.test.
(is (thrown-ex-info-type? ::foo (throw (ex-info "boom" {:type ::foo})))) -> true
It dispatches via try+/catch
, so relations are supported.
we support turning these kind of ex-infos into data via clojure.core.protocols/datafy
and back into exceptions via exoscale.ex/map->ex-info
.
=> (clojure.core.protocols/datafy (ex/ex-incorrect "boom" {:a 1} (ex/ex-incorrect "the-cause")))
#:exoscale.ex{:type :exoscale.ex/incorrect
:message "boom"
:data {:a 1}
:deriving #{:exoscale.ex/foo :exoscale.ex/bar}
:cause #:exoscale.ex{:type :exoscale.ex/incorrect
:message "the-cause"
:data {...}
:deriving #{:exoscale.ex/foo :exoscale.ex/bar}}
=> (type (ex/map->ex-info *1))
clojure.lang.ExceptionInfo
Some real life examples of usage for this:
Deny all display of user exceptions to the end-user by default via
top level middleware and only let through the ones marked safe to
show via a derived type
.
Skip sentry logging for some kind of exceptions (or the inverse)
Can you improve this documentation? These fine people already did:
Max Penet, wdullaer, Marc Falzon & Pierre-Yves RitschardEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close