Skip to content

Instantly share code, notes, and snippets.

@camsaul
Last active June 24, 2020 18:04
Show Gist options
  • Save camsaul/1a11e2a70d4a6b53e0c68d7c88745533 to your computer and use it in GitHub Desktop.
Save camsaul/1a11e2a70d4a6b53e0c68d7c88745533 to your computer and use it in GitHub Desktop.
Cam's try+ macro
(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))))
@camsaul
Copy link
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)"

@camsaul
Copy link
Author

camsaul commented Jun 24, 2020

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