Last active
June 24, 2020 18:04
-
-
Save camsaul/1a11e2a70d4a6b53e0c68d7c88745533 to your computer and use it in GitHub Desktop.
Cam's try+ macro
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(defn all-ex-data | |
"Combined `ex-data` from all instances of `ExceptionInfo`. Prefers keys from lower-level Exceptions (e.g. the root | |
cause)." | |
[e] | |
(->> (iterate ex-cause e) | |
(take-while some?) | |
(map ex-data) | |
(reduce merge))) | |
(defn- catch-form? [x] | |
(and (sequential? x) | |
(= (first x) 'catch))) | |
(defn- keyword-catch-form? [x] | |
(and (catch-form? x) | |
(keyword? (second x)))) | |
(defn- combine-keyword-catch-forms | |
"Combine a series of keyword catch forms like | |
(catch ::x e1 ...) | |
(catch ::y e2 ...) | |
Into a conditional statement like | |
(let [data (all-ex-data &e)] | |
(cond | |
(isa? (:type data) ::x) | |
(let [e1 &e] | |
...) | |
(isa? (:type data) ::y) | |
(let [e2 &e] | |
...) | |
:else | |
else-clause)) | |
`else-clause` is the final `else` clause to execute if none of the keywords match." | |
[keyword-catch-forms else-form] | |
(let [data-binding (gensym "data-")] | |
`(let [~data-binding (all-ex-data ~'&e)] | |
(cond | |
~@(mapcat | |
(fn [[_ k binding & body]] | |
[`(isa? (:type ~data-binding) ~k) | |
`(let [~binding ~data-binding] | |
~@body)]) | |
keyword-catch-forms) | |
:else | |
~else-form)))) | |
(defn- combine-class-catch-forms | |
"Combine a series of class catch forms like | |
(catch clojure.lang.ExceptionInfo e1 ...) | |
(catch Throwable e2 ...) | |
Into a conditional statement like | |
(cond | |
(instance? clojure.lang.ExceptionInfo &e) | |
(let [e1 &e] | |
...) | |
(instance? Throwable &e) | |
(let [e2 &e] | |
...) | |
:else | |
(throw &e)) | |
If none of the class catch forms match, rethrow the Exception." | |
[class-catch-forms] | |
`(cond | |
~@(mapcat | |
(fn [[_ klass binding & body]] | |
[`(instance? ~klass ~'&e) `(let [~binding ~'&e] | |
~@body)]) | |
class-catch-forms) | |
:else | |
~'(throw &e))) | |
(defn- catch* | |
"Combine sequences of keyword and class catch forms into a single `catch` form that handles all of them correctly." | |
[keyword-catch-forms class-catch-forms] | |
(let [class-form (combine-class-catch-forms class-catch-forms) | |
keyword-form (combine-keyword-catch-forms keyword-catch-forms class-form)] | |
`(catch Throwable ~'&e | |
~keyword-form))) | |
(defmacro try+ | |
"Like normal `try`, but also allows you to use `catch` forms with keywords instead of classes; these keywords compare | |
the `:type` key in `ex-data` maps using `isa?`. Looks thru the entire Exception chain and uses the lowest-level | |
`:type` key. | |
In keyword `catch` forms, all `ex-data` maps for the entire Exception chain are combined into a single map using | |
`all-ex-data` and bound, which means you can destructure the `ex-data` directly in the `catch` binding: | |
(try+ | |
(throw (Exception. \"(Wrapped)\" (ex-info \"oops\" {:type ::error, :x 1}))) | |
(catch ::error {:keys [x]} | |
x)) | |
;; -> 1 | |
The caught Exception is bound to the anaphor `&e` if you need to access it." | |
{:style/indent 0} | |
[& forms] | |
(let [[body-forms catch-forms] (split-with (complement catch-form?) forms) | |
keyword-catch-forms (filter keyword-catch-form? catch-forms) | |
class-catch-forms (remove keyword-catch-form? catch-forms)] | |
`(try | |
~@body-forms | |
~(catch* keyword-catch-forms class-catch-forms)))) |
Author
camsaul
commented
Jun 24, 2020
(defn x []
(try+
(+ 1 2)
(throw (Exception. "(Wrapping ex-info)" (ex-info "oops" {:type ::terraform-error, :x 1})))
(catch ::nasty-terraform-error {:keys [x]}
(str "Nasty error: " x))
(catch ::any-error {:keys [x]}
(format "Error: %s %s" x (.getMessage &e)))
(catch clojure.lang.ExceptionInfo _
"Ex-info")
(catch Throwable _
"Throwable.")))
;; -> "Error: 1 (Wrapping ex-info)"
Macroexpansion:
(try
(+ 1 2)
(throw (Exception. "(Wrapping ex-info)" (ex-info "oops" {:type ::terraform-error, :x 1})))
(catch java.lang.Throwable &e
(let [data-87233 (all-ex-data &e)]
(cond
(isa? (:type data-87233) ::nasty-terraform-error)
(let [{:keys [x]} data-87233]
(str "Nasty error: " x))
(isa? (:type data-87233) ::any-error)
(let [{:keys [x]} data-87233]
(format "Error: %s %s" x (.getMessage &e)))
:else
(cond
(instance? clojure.lang.ExceptionInfo &e)
(let [_ &e]
"Ex-info")
(instance? Throwable &e)
(let [_ &e]
"Throwable.")
:else
(throw &e))))))
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment